Merge pull request #193 from jk-cluster/feat/teamspeak-server-notes-and-157
teamspeak-server: NOTES.txt and the exponential-quantity fix (5.1.0+up6.0.0.beta12.1)
This commit is contained in:
3 files changed
+261
-3
No files matched your search
@@ -5,5 +5,5 @@ icon: https://avatars.githubusercontent.com/u/136759148
|
||||
|
||||
type: application
|
||||
|
||||
version: 5.0.0+up6.0.0.beta12.1
|
||||
version: 5.1.0+up6.0.0.beta12.1
|
||||
appVersion: "6.0.0-beta12.1"
|
||||
@@ -0,0 +1,241 @@
|
||||
{{- $replicas := include "teamspeak-server.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) -}}
|
||||
{{- $res := fromYaml (include "teamspeak-server.effectiveResources" .Values.resources) -}}
|
||||
{{- $eph := index ($res.limits | default dict) "ephemeral-storage" | default "the computed ephemeral-storage limit" -}}
|
||||
{{- $startup := fromYaml (include "teamspeak-server.startupProbe" .Values.startupProbe) -}}
|
||||
{{- $budget := "" -}}
|
||||
{{- if and (kindIs "float64" $startup.periodSeconds) (kindIs "float64" $startup.failureThreshold) -}}
|
||||
{{- $budget = printf "%.0f seconds" (mulf $startup.periodSeconds $startup.failureThreshold) -}}
|
||||
{{- end -}}
|
||||
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
||||
{{- $sfs := printf "%s-%s-sfs" .Release.Name .Chart.Name -}}
|
||||
{{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}}
|
||||
{{- $pvc := printf "%s-teamspeak-volume-claim" .Release.Name -}}
|
||||
{{- $voice := .Values.server.voicePort -}}
|
||||
{{- $ft := .Values.server.fileTransferPort -}}
|
||||
{{- $badPorts := list -}}
|
||||
{{- if or (lt (int $voice) 30000) (gt (int $voice) 32767) }}{{- $badPorts = append $badPorts (printf "server.voicePort (%v)" $voice) }}{{- end }}
|
||||
{{- if or (lt (int $ft) 30000) (gt (int $ft) 32767) }}{{- $badPorts = append $badPorts (printf "server.fileTransferPort (%v)" $ft) }}{{- end }}
|
||||
TeamSpeak {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
||||
|
||||
ACCESS — TWO NODE PORTS, NO INGRESS
|
||||
TeamSpeak is not HTTP, so this chart renders no Ingress and no certificate.
|
||||
The Service {{ $svc }} is of type NodePort, so both
|
||||
ports are open on EVERY node of the cluster and reachable by anyone who can
|
||||
reach a node.
|
||||
|
||||
voice node IP : {{ $voice }}/UDP (server.voicePort)
|
||||
file transfer node IP : {{ $ft }}/TCP (server.fileTransferPort)
|
||||
|
||||
kubectl get nodes -o wide # the IPs to hand to your users
|
||||
|
||||
Each of the two numbers is used THREE times: as the container port, as the
|
||||
Service port and as the nodePort. That is deliberate — TeamSpeak's file
|
||||
transfer announces its own port to the client, so the port inside the
|
||||
container has to be the same one the client reaches from outside. It also
|
||||
means the voice port cannot be 9987, the port a client assumes when it is
|
||||
given a bare host name. Tell your users the full address:
|
||||
|
||||
NODE-IP:{{ $voice }}
|
||||
{{- if $badPorts }}
|
||||
|
||||
OUT OF THE NODE-PORT RANGE: {{ join ", " $badPorts }}
|
||||
A default cluster only allocates node ports from 30000-32767. The values
|
||||
schema accepts 1-65535, so this rendered fine — the API server is what
|
||||
rejects it, when you apply the Service, with "provided port is not in the
|
||||
valid range". Nothing about that error mentions this chart. Either pick a
|
||||
port inside the range or widen --service-node-port-range on the API
|
||||
server.
|
||||
{{- end }}
|
||||
|
||||
Inside the cluster the same ports apply; there is no separate service port:
|
||||
|
||||
{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local:{{ $voice }}
|
||||
|
||||
THE FIRST START PRINTS TWO SECRETS AND NEVER PRINTS THEM AGAIN
|
||||
Against an empty data volume the server creates its database, and with it a
|
||||
virtual server, a ServerQuery admin account and a ServerAdmin privilege key.
|
||||
All of it goes to stdout ONCE, at that first start only, and into no Secret:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs {{ $pod }}
|
||||
|
||||
Two different things are printed, and they are not equally recoverable:
|
||||
|
||||
token=... the ServerAdmin privilege key. This is what
|
||||
you paste into a connected client to become
|
||||
server admin. Also written to the log FILES
|
||||
on the data volume, so it survives a lost
|
||||
stdout:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} exec {{ $pod }} -- \
|
||||
sh -c 'grep -h "token=" /var/tsserver/logs/*.log'
|
||||
|
||||
serveradmin / apikey the ServerQuery credentials. These go to
|
||||
stdout ONLY — they are not in the log files
|
||||
on the volume. Once the pod's stdout is
|
||||
gone, they are gone.
|
||||
|
||||
Losing the privilege key without having claimed admin rights means starting
|
||||
over: the identity that holds admin is stored in the database, so the way
|
||||
back is deleting the volume and letting a new server be created.
|
||||
|
||||
A second start against an existing volume prints neither. That includes
|
||||
every ordinary restart — a rollout, a node drain, an app-version bump. If a
|
||||
fresh install seems to be missing its token, check whether the PVC below
|
||||
already existed.
|
||||
|
||||
server.voicePort ONLY TAKES EFFECT ON A BRAND-NEW VOLUME
|
||||
This is the trap of this chart, and it is silent in both directions.
|
||||
|
||||
The chart passes server.voicePort to the container as TSSERVER_DEFAULT_PORT.
|
||||
The server reads that variable only when it CREATES the virtual server; from
|
||||
then on the port lives in the virtual server's row in
|
||||
/var/tsserver/tsserver.sqlitedb and the variable is ignored. Measured on
|
||||
{{ .Chart.AppVersion }}: a restart against an existing volume with a changed
|
||||
TSSERVER_DEFAULT_PORT kept listening on the old port.
|
||||
|
||||
So changing server.voicePort on a release that already has data moves the
|
||||
Service port, the nodePort and the containerPort to the new number, while
|
||||
the server inside the pod keeps answering on the old one. Voice becomes
|
||||
unreachable, and — see below — every probe stays green.
|
||||
|
||||
There is no comfortable way out of that. The port is a property of the
|
||||
virtual server in the database, and the only interface that can edit it is
|
||||
ServerQuery, which this chart does not expose (see below). Treat
|
||||
server.voicePort as fixed for the life of the data volume: pick it before
|
||||
the first install, and if it really has to change, plan for a new volume and
|
||||
a new server.
|
||||
|
||||
server.fileTransferPort behaves the opposite way: TSSERVER_FILE_TRANSFER_PORT
|
||||
is applied on every start, so that one really does follow the value.
|
||||
{{- if eq $replicas "0" }}
|
||||
|
||||
SCALED TO ZERO
|
||||
{{ if .Values.scaleDown }}scaleDown: true{{ else }}replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so nothing
|
||||
answers on the ports above until you set it back and upgrade again. The data
|
||||
PVC and the Service stay in place, so no data is lost and the node ports
|
||||
stay reserved.
|
||||
{{- end }}
|
||||
{{- if gt (atoi $replicas) 1 }}
|
||||
|
||||
MORE THAN ONE REPLICA WILL NOT WORK
|
||||
replicas is {{ $replicas }}. Every replica mounts the same ReadWriteOnce claim
|
||||
{{ $pvc }} — this chart writes a plain PVC, not a
|
||||
StatefulSet volumeClaimTemplate, so there is no per-pod volume. Unless the
|
||||
storage class hands that claim to several nodes at once, the extra pods
|
||||
never start; where they do, two servers open the same SQLite database and
|
||||
corrupt it. The Service would also spray a client's UDP packets across both
|
||||
pods, which no TeamSpeak session survives. This chart is built for one
|
||||
replica.
|
||||
{{- end }}
|
||||
|
||||
THE BUNDLED LICENSE EXPIRES ON 2026-10-01
|
||||
This image ships without a license file and falls back to the built-in one.
|
||||
Measured by running {{ .Chart.AppVersion }}:
|
||||
|
||||
type : No License
|
||||
starting date : Wed Jul 1 00:00:00 2026
|
||||
ending date : Thu Oct 1 00:00:00 2026
|
||||
max virtualservers: 1
|
||||
max slots : 32
|
||||
|
||||
Two consequences. The 32-slot ceiling is the real capacity of this release,
|
||||
whatever the resources say. And after the ending date the server refuses to
|
||||
start — a working deployment turns into CrashLoopBackOff with no change on
|
||||
your side. The fix is an app-version bump to a newer beta, which ships a
|
||||
license with a later date; that is tracked separately and is not something a
|
||||
values change can solve. Confirm the dates for the version you actually run:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs {{ $pod }} | grep -A6 "Licensing Information"
|
||||
|
||||
NO SECRETS, AND NO QUERY INTERFACE TO USE THEM ON
|
||||
This chart creates no OnePasswordItem and no Kubernetes Secret — there is no
|
||||
secrets.* value to set, and nothing to prepare in a vault before installing.
|
||||
The server's own credentials are the ones printed at first start.
|
||||
|
||||
Which matters, because those ServerQuery credentials have nowhere to go. On
|
||||
a default install nothing listens on a query port: measured on
|
||||
{{ .Chart.AppVersion }}, the only listening sockets in the container are
|
||||
UDP {{ $voice }} (voice) and TCP {{ $ft }} (file transfer). The pod
|
||||
declares a third container port, web-query 10080, and nothing binds it; it
|
||||
is also absent from the Service. The server additionally restricts query
|
||||
access to 127.0.0.1 and ::1 by default. Administration therefore happens
|
||||
through a connected TeamSpeak client with the privilege key, not over
|
||||
ServerQuery.
|
||||
|
||||
WHAT A READY POD DOES NOT PROVE
|
||||
All three probes are bare TCP connects to the file-transfer port. Green
|
||||
means one thing: the file-transfer listener accepts a connection.{{ if $budget }} The startup
|
||||
probe covers {{ $budget }} and keeps liveness and readiness off until then.{{ end }}
|
||||
|
||||
It says nothing about voice, which is the actual service — UDP cannot be
|
||||
probed this way, so no probe in this chart ever touches it. A release can be
|
||||
fully Ready while the voice port is wrong (see server.voicePort above), while
|
||||
the virtual server is stopped, or while every slot is taken. It says nothing
|
||||
about the license either, though an expired one crashes the process and does
|
||||
surface as a restart loop.
|
||||
|
||||
The real check is a client connecting to NODE-IP:{{ $voice }}. Short of
|
||||
that, the server's own view:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs {{ $sfs }} | grep "listening on"
|
||||
|
||||
which prints the ports the server actually bound — compare them with the two
|
||||
at the top of these notes. They should match; when they do not, the cause is
|
||||
almost always the port trap above.
|
||||
|
||||
THE DATA VOLUME IS THE SERVER, AND IT BELONGS TO THIS RELEASE
|
||||
Everything is in one directory, /var/tsserver, on the claim
|
||||
{{ $pvc }} ({{ .Values.storage.size }}{{ if .Values.storage.storageClass }} from storageClass {{ .Values.storage.storageClass }}{{ end }}):
|
||||
|
||||
tsserver.sqlitedb the virtual server, its port, all permissions,
|
||||
identities, channels and ServerQuery accounts
|
||||
files/ everything uploaded through file transfer
|
||||
logs/ the server's own logs, incl. the privilege key
|
||||
query_ip_*list.txt the query allow/deny lists
|
||||
|
||||
Note the claim name: {{ .Release.Name }}-teamspeak-volume-claim, without
|
||||
the chart name. That is a historical name this chart keeps deliberately —
|
||||
renaming it would orphan the data.
|
||||
|
||||
It is an ordinary PVC of this release, not a volumeClaimTemplate, and it
|
||||
carries no resource-policy annotation. "helm uninstall {{ .Release.Name }}"
|
||||
deletes it, and with a Delete reclaim policy the whole server goes with it —
|
||||
not just chat history, but the admin identity and the privilege key. Back it
|
||||
up before uninstalling; an annotation added to the live PVC afterwards does
|
||||
not help, because Helm reads that annotation from the release manifest.
|
||||
|
||||
Growing storage.size only works if the storage class allows volume
|
||||
expansion. Shrinking is refused by Kubernetes and makes the next upgrade
|
||||
fail on the PVC instead of touching the data.
|
||||
|
||||
THE ROOT FILESYSTEM IS READ-ONLY AND THERE IS NO SCRATCH VOLUME
|
||||
The container runs non-root as uid/gid 9987 with readOnlyRootFilesystem and
|
||||
all capabilities dropped, and /var/tsserver is the only writable path — the
|
||||
chart mounts no emptyDir. Verified against {{ .Chart.AppVersion }}: the
|
||||
server needs nothing else, so this is the supported configuration and not a
|
||||
deviation to "fix". An image that later wants a writable /tmp would fail at
|
||||
start, not at build time.
|
||||
|
||||
fsGroup 9987 is what makes the volume writable. On a storage class that does
|
||||
not honour fsGroup the pod starts and the server then cannot open its
|
||||
database — that failure is in the pod log, not in the events.
|
||||
{{- if .Values.storage.storageClass }} This release uses "{{ .Values.storage.storageClass }}".{{ end }}
|
||||
Whatever the container writes outside the volume is nothing, so the
|
||||
ephemeral-storage limit ({{ $eph }}) is headroom rather than a
|
||||
working budget.
|
||||
{{- if .Values.env }}
|
||||
|
||||
env IS APPENDED, NOT MERGED — AND CAN SHADOW THE CHART'S PORTS
|
||||
The {{ len .Values.env }} entr{{ if eq (len .Values.env) 1 }}y{{ else }}ies{{ end }} here land{{ if eq (len .Values.env) 1 }}s{{ end }} AFTER the three variables the chart
|
||||
always sets: TSSERVER_LICENSE_ACCEPTED, TSSERVER_DEFAULT_PORT and
|
||||
TSSERVER_FILE_TRANSFER_PORT. Kubernetes keeps the last value for a repeated
|
||||
name, so redefining either port variable here wins inside the container while
|
||||
the Service, the nodePort and the containerPort still follow server.*. That
|
||||
splits the two apart with nothing reporting it — set the ports under server,
|
||||
never through env.
|
||||
|
||||
Values are rendered through tpl, so they may contain template expressions.
|
||||
Anything sensitive belongs in valueFrom.secretKeyRef: a plain "value" is
|
||||
stored in the pod spec and readable by everyone who can read pods in
|
||||
{{ .Release.Namespace }}.
|
||||
{{- end }}
|
||||
@@ -67,7 +67,7 @@ Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary
|
||||
results are printed without a decimal point.
|
||||
*/}}
|
||||
{{- define "teamspeak-server.multiplyQuantity" -}}
|
||||
{{- $quantity := printf "%v" .quantity -}}
|
||||
{{- $quantity := include "teamspeak-server.formatNumber" .quantity -}}
|
||||
{{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}}
|
||||
{{- if not $number -}}
|
||||
{{- fail (printf "cannot parse resource quantity %q" $quantity) -}}
|
||||
@@ -80,7 +80,24 @@ results are printed without a decimal point.
|
||||
{{- if eq $product (floor $product) -}}
|
||||
{{- printf "%d%s" (int64 $product) $suffix -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v%s" $product $suffix -}}
|
||||
{{- printf "%s%s" (include "teamspeak-server.formatNumber" $product) $suffix -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Render a number as a plain decimal string, never in exponential notation.
|
||||
|
||||
Why this exists (issue #157): the null-safe fallback helpers pass values
|
||||
through fromYaml, which parses every number as float64. Go's "%v" switches
|
||||
float64 to exponential notation from about 1e6 upwards, so a bare byte count
|
||||
like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the
|
||||
render aborted. Strings ("512Mi") are passed through untouched.
|
||||
*/}}
|
||||
{{- define "teamspeak-server.formatNumber" -}}
|
||||
{{- if kindIs "float64" . -}}
|
||||
{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%v" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
Reference in new issue
Block a user