Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 23s
Validate Helm Charts / Bump level (pull_request) Successful in 4s
Validate Helm Charts / Selector match (pull_request) Successful in 12s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 10s
Validate Helm Charts / YAML scalars (pull_request) Successful in 9s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 10s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 23s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 14s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 6s
Validate Helm Charts / Build bulwark (pull_request) Successful in 6s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 5s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 5s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 10s
Validate Helm Charts / Build garage (pull_request) Successful in 6s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 11s
Validate Helm Charts / Build keycloak (pull_request) Successful in 5s
Validate Helm Charts / Build minecraft-router (pull_request) Successful in 8s
Validate Helm Charts / Build patchmon (pull_request) Successful in 8s
Validate Helm Charts / Build rallly (pull_request) Successful in 5s
Validate Helm Charts / Build satisfactory (pull_request) Successful in 5s
Validate Helm Charts / Build minecraft-server (pull_request) Successful in 7s
Validate Helm Charts / Build outline (pull_request) Successful in 5s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 5s
Validate Helm Charts / Build samba (pull_request) Successful in 5s
Validate Helm Charts / Build searxng (pull_request) Successful in 5s
Validate Helm Charts / Build sure (pull_request) Successful in 11s
Validate Helm Charts / Build teamspeak-server (pull_request) Successful in 18s
Validate Helm Charts / Build template-chart (pull_request) Successful in 12s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 4s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 5s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 5s
Validate Helm Charts / Build zipline (pull_request) Successful in 4s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 13s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 15s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 12s
Validate Helm Charts / Trivy config minecraft-router (pull_request) Successful in 17s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 11s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 13s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 20s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 14s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 11s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 14s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 15s
Validate Helm Charts / Trivy config minecraft-server (pull_request) Successful in 27s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 12s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 13s
Validate Helm Charts / Trivy config teamspeak-server (pull_request) Successful in 11s
Validate Helm Charts / Trivy config samba (pull_request) Successful in 16s
Validate Helm Charts / Trivy config satisfactory (pull_request) Successful in 22s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 22s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 17s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 11s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 12s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 17s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 18s
Install-test / Install-test bulwark (pull_request) Successful in 1m18s
Install-test / Install-test homeassistant (pull_request) Successful in 2m3s
Install-test / Install-test outline (pull_request) Successful in 2m9s
Install-test / Install-test rss-bridge (pull_request) Successful in 51s
Install-test / Install-test samba (pull_request) Successful in 1m14s
Install-test / Install-test apache-tika (pull_request) Successful in 1m7s
Install-test / Install-test bentopdf (pull_request) Successful in 1m5s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 1m47s
Install-test / Install-test cyberchef (pull_request) Successful in 49s
Install-test / Install-test garage (pull_request) Successful in 1m10s
Install-test / Install-test gotenberg (pull_request) Successful in 1m35s
Install-test / Install-test keycloak (pull_request) Successful in 2m23s
Install-test / Install-test minecraft-router (pull_request) Successful in 37s
Install-test / Install-test minecraft-server (pull_request) Successful in 1m34s
Install-test / Install-test patchmon (pull_request) Successful in 1m17s
Install-test / Install-test rallly (pull_request) Successful in 1m30s
Install-test / Install-test searxng (pull_request) Successful in 1m9s
Install-test / Install-test sure (pull_request) Successful in 2m10s
Install-test / Install-test voucher-vault (pull_request) Successful in 1m48s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 59s
Install-test / Install-test zipline (pull_request) Successful in 1m59s
Validate Helm Charts / Install-test (pull_request) Successful in 33m44s
Install-test / Install-test teamspeak-server (pull_request) Successful in 1m15s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m51s
garage, minecraft-router, minecraft-server, samba, teamspeak-server,
satisfactory. One major each. These are the charts the plan said to leave for
last, and they earned it - three of them changed the convention rather than
just consuming it.
GARAGE is the clearest illustration after keycloak. Its four ports split three
ways: s3 and web are RESTRICTED by source (no probe touches them), admin is the
probe path and stays open inside the cluster - which is uncomfortable, because
admin is also the admin API, and that is said out loud instead of glossed - and
rpc gets the narrowest rule the object can express, a podSelector with no
namespaceSelector. All four numbers come from garage.settings.
MINECRAFT-SERVER is the tightest policy in the repository, and the reason is
worth knowing: all three of its probes are EXEC probes, which put no packet on
the network, so no port has to stay open for the kubelet and no probeSources
value is needed at all. RCON and metrics are restricted outright. The game port
has two branches rendered from the same value the Service reads -
server.service.type NodePort means players arrive from the internet and the
rule must be open; anything else means the only path is in-cluster, so the rule
is restricted and the router's namespace has to be in allowedNamespaces.
MINECRAFT-ROUTER derives its egress from the routing table instead of guessing:
server.backendPort plus the "port" override of every mapping that sets one. Add
a backend on a non-standard port and the policy follows. What it cannot express
is the destination - mappings name hosts, and this object matches no hostname -
so the rule is wider than the routing table, and that is written down.
SATISFACTORY gets policyTypes: [Ingress] and no egress section, and the second
half of the reason is the deciding one. Steam's content delivery is not a
pinnable port set (SteamCMD over TCP 80/443/27015-27050, master servers over
UDP 27000-27100 and 4380, changed by Valve, re-downloaded on every update) -
and this is the ONE chart with no install-test at all, because the image is
multiple GB and the first start pulls over 10GB. An egress cap here would be
the only one in the sweep with no empirical check behind it, on the chart whose
correct port set is least knowable.
SAMBA and TEAMSPEAK-SERVER carry the NodePort lesson explicitly: the rules name
445/139 and the pod's voice and file-transfer ports, never the NodePorts, and
teamspeak's file is explicit that its pod port and NodePort happening to share a
number is a coincidence of its values rather than a pattern to copy. Teamspeak's
voice rule is UDP, and the file says why that matters: a TCP rule on the same
number is accepted, looks complete, and drops every voice packet.
THREE THINGS THE CHECKS CAUGHT WHILE THIS BATCH WAS WRITTEN, all of them real:
- minecraft-server's RCON rule named a port the pod does not declare. The
containerPort is gated on server.rcon.enabled and the rule was not, so
assertion 2 rejected it. The rule now carries the same gate.
- samba and satisfactory would have shipped allowedNamespaces and
probeSources values that reach NO rule - every one of their inbound rules
is open to any source. A value that silently does nothing is worse than one
that is absent, because it reads as a control, so both are gone from those
two charts and from their schemas, with the reason in values.yaml.
teamspeak-server loses probeSources for the same reason and keeps
allowedNamespaces, which its web-query rule uses.
- the policy templates no longer declare template variables they do not use.
samba keeps one, satisfactory none.
Part of #5
195 lines
11 KiB
Plaintext
195 lines
11 KiB
Plaintext
{{- $replicas := include "minecraft-router.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.server.replicas) -}}
|
|
{{- $outward := .Values.server.outwardPort | default 25565 -}}
|
|
{{- $port := .Values.server.port | default 25565 -}}
|
|
{{- $backendPort := .Values.server.backendPort | default 25565 -}}
|
|
{{- $apiPort := .Values.server.apiPort | default 8080 -}}
|
|
{{- $deploy := printf "%s-%s-deployment" .Release.Name .Chart.Name -}}
|
|
{{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}}
|
|
{{- $cm := printf "%s-%s-configmap" .Release.Name .Chart.Name -}}
|
|
{{- $mappings := .Values.mappings | default list -}}
|
|
mc-router {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
|
|
|
ACCESS — A TCP PROXY, NOT A WEB SERVICE
|
|
This release has no Ingress, no certificate and no URL. Minecraft clients
|
|
speak raw TCP to the NodePort Service {{ $svc }}:
|
|
|
|
<any-node-ip>:{{ $outward }}
|
|
|
|
server.outwardPort ({{ $outward }}) is the port on the node and the one that goes into
|
|
the client. server.port ({{ $port }}) is what mc-router listens on inside the pod and
|
|
what the Service publishes cluster-internally; changing it does not change
|
|
what a player dials.
|
|
|
|
Which backend a connection reaches is decided by the HOSTNAME the client
|
|
typed, not by the port: mc-router reads the server address out of the
|
|
Minecraft handshake and looks it up in the routing table below. Every mapped
|
|
name therefore has to resolve to a node of this cluster (or to whatever sits
|
|
in front of it), and the player has to enter that name. Connecting to the
|
|
node's bare IP address carries no matching hostname and is routed nowhere.
|
|
{{- if or (lt (int $outward) 30000) (gt (int $outward) 32767) }}
|
|
|
|
{{ $outward }} IS OUTSIDE THE DEFAULT NODE-PORT RANGE (30000-32767)
|
|
This install needs a cluster whose API server runs with an extended
|
|
--service-node-port-range; without it the API server rejects the Service and
|
|
the release fails on that one object while everything else applies. The
|
|
choice is deliberate — 25565 is the port Minecraft clients dial by default.
|
|
Confirm the Service actually exists:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get svc {{ $svc }}
|
|
{{- end }}
|
|
|
|
ROUTING TABLE — {{ len $mappings }} MAPPING(S) ACTIVE IN THIS RELEASE
|
|
{{ range $mapping := $mappings }} {{ $mapping.domain }} → {{ $mapping.internalUrl }}:{{ $mapping.port | default $backendPort }}{{ if $mapping.port }} (own port, overrides server.backendPort {{ $backendPort }}){{ end }}
|
|
{{ end }}
|
|
mappings is the ONLY source of routes here. The chart does not run mc-router
|
|
in its Kubernetes auto-discovery mode — there is no ServiceAccount and
|
|
automountServiceAccountToken is false, so the router cannot watch the API and
|
|
never reads routing annotations on a backend Service. A server announced that
|
|
way stays unreachable through this router until its name stands in mappings,
|
|
and a name that is in mappings routes there no matter what any annotation
|
|
elsewhere says. Where both exist, this table wins.
|
|
|
|
The table is rendered into the ConfigMap {{ $cm }}
|
|
(key custom-routing-config.json) and mounted with subPath at
|
|
/data/custom-routing-config.json. Two consequences:
|
|
|
|
- mc-router reads that file once at startup, and the kubelet never
|
|
refreshes a subPath mount, so a routing change takes effect only through
|
|
a pod restart. The Deployment carries a checksum/config annotation, so
|
|
helm upgrade performs that restart for you — at the price of a few
|
|
seconds in which no client can connect and open sessions are dropped.
|
|
Treat a routing change as a short, planned outage.
|
|
- internalUrl is dialed by name. Point it at the backend's Service
|
|
(survival-service.minecraft.svc.cluster.local), not at a pod IP: the
|
|
Service name survives a restart of the Minecraft server, the pod IP does
|
|
not.
|
|
|
|
There is no fallback backend. The chart configures no default route, so a
|
|
client arriving with a hostname outside the table is not connected to
|
|
anything. mc-router itself knows the concept (its API has POST /defaultRoute)
|
|
and parses every flag equally as an environment variable, so server.env is
|
|
where you would set one.
|
|
|
|
WHAT A READY ROUTER DOES NOT PROVE
|
|
All three probes call mc-router's own API — GET /routes on container port
|
|
api-port ({{ $apiPort }}) — which answers as soon as the config file has been read. It
|
|
answers just as happily when every backend behind it is down, and when a
|
|
domain or an internalUrl in the table is misspelled: the router is up, the
|
|
mapping is loaded, and the player still cannot connect. A green rollout here
|
|
is a statement about the proxy, never about a Minecraft server. Nothing in
|
|
this release watches the backends.
|
|
|
|
A backend that is unreachable also does not restart the router: mc-router
|
|
accepts the client, fails to dial the target and closes the connection, which
|
|
the player sees as an ordinary connection failure. The router's log is the
|
|
only place that names the address it tried:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs deploy/{{ $deploy }}
|
|
|
|
The API is not published by the Service (which carries proxy-port only), so
|
|
ask the pod directly:
|
|
|
|
kubectl -n {{ .Release.Namespace }} port-forward deploy/{{ $deploy }} {{ $apiPort }}:{{ $apiPort }}
|
|
curl -s -H 'Accept: application/json' http://127.0.0.1:{{ $apiPort }}/routes
|
|
|
|
That is the same endpoint the probes use; it returns the live table, and
|
|
/metrics returns the Prometheus metrics. The API also writes: POST /routes,
|
|
POST /defaultRoute and DELETE /routes/{addr} change the routing of the
|
|
RUNNING PROCESS only. Nothing written that way survives a restart — the
|
|
mounted ConfigMap is the source of truth and the next rollout silently
|
|
reinstates it. Fix routing in mappings, not over the API.
|
|
|
|
NO SECRETS, NO VOLUMES, NO STATE
|
|
This chart creates no OnePasswordItem and mounts no Secret, so there is no
|
|
1Password item to prepare and no field names to get right. The only mounted
|
|
object is the routing ConfigMap, rendered entirely from mappings:
|
|
"helm uninstall {{ .Release.Name }}" destroys nothing that is not already in your values
|
|
file. The worlds live in the backend releases, not here.
|
|
{{- if eq $replicas "0" }}
|
|
|
|
SCALED TO ZERO
|
|
{{ if .Values.scaleDown }}scaleDown: true{{ else }}server.replicas: 0{{ end }} — no router pod runs, so every hostname above stops
|
|
connecting although the Service and the ConfigMap stay in place. The backend
|
|
Minecraft servers keep running and stay reachable through their own Services;
|
|
only the way in through this router is gone.
|
|
{{- end }}
|
|
{{- if gt (atoi $replicas) 1 }}
|
|
|
|
MORE THAN ONE ROUTER REPLICA
|
|
server.replicas is {{ $replicas }}. mc-router keeps no state between connections and every
|
|
replica mounts the same routing ConfigMap, so the Service can spread clients
|
|
across them. Two things change: an established connection is pinned to the
|
|
replica that accepted it, so a rollout drops the open sessions pod by pod;
|
|
and a runtime change through the API reaches only the one pod you forwarded
|
|
to, leaving the replicas disagreeing until the next restart.
|
|
{{- end }}
|
|
{{- if .Values.server.env }}
|
|
|
|
server.env IS APPENDED TO WHAT THE CHART SETS
|
|
The entries land after ROUTES_CONFIG and are rendered through tpl, so a value
|
|
may contain template expressions. Because mc-router parses its flags with
|
|
go-flagsfiller, this is also the place for every option the chart does not
|
|
model explicitly — LOG_LEVEL, BACKEND_DIAL_TIMEOUT, CONNECTION_RATE_LIMIT.
|
|
|
|
Two things it will not do. The chart passes --port and --api-binding as
|
|
command-line arguments, and those take precedence over the matching
|
|
environment variables, so PORT or API_BINDING set here change nothing — use
|
|
server.port and server.apiPort. And a plain "value" is stored in the pod spec
|
|
in clear text, readable by everyone who can read pods in {{ .Release.Namespace }}; anything
|
|
sensitive belongs in valueFrom.secretKeyRef.
|
|
{{- end }}
|
|
{{- if .Values.networkPolicy.enabled }}
|
|
|
|
NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT
|
|
{{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods.
|
|
Inbound: the proxy port (TCP {{ .Values.server.port }}, open — it is published as NodePort
|
|
{{ .Values.server.outwardPort }} and the senders are players on the internet) and the API port
|
|
(TCP {{ .Values.server.apiPort }}, the probe path). Outbound: DNS plus the backend ports taken from
|
|
the routing table itself — TCP {{ $ports := list (int .Values.server.backendPort) }}{{ range .Values.mappings }}{{ if .port }}{{ $ports = append $ports (int .port) }}{{ end }}{{ end }}{{ join ", " ($ports | uniq) }}{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
|
|
|
|
THE BACKEND RULE IS BY PORT, NOT BY DESTINATION. mappings name hosts, and
|
|
this object cannot match a hostname — so the rule permits those ports to any
|
|
address, which is wider than your routing table. Adding a backend on a
|
|
non-standard port needs no change here: the policy reads the same value the
|
|
router does.
|
|
|
|
Three limits belong in the same breath, because none of them is visible in
|
|
the object itself:
|
|
|
|
- EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
|
|
selectors and cannot express a hostname, so every egress rule permits its
|
|
port to ANY address. That is a limit of the object, not a shortcut — this
|
|
cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An
|
|
outbound port the chart does not know about is BLOCKED: add it to
|
|
networkPolicy.egress.extraPorts, and expect to find out through an
|
|
application-level connection error rather than through this object.
|
|
{{- if .Values.networkPolicy.probeSources }}
|
|
- THE PROBE PATH IS RESTRICTED in this release, because
|
|
networkPolicy.probeSources is set to
|
|
{{ join ", " .Values.networkPolicy.probeSources }}
|
|
Probes come from the IP of the WORKER NODE running the pod. If none of those
|
|
CIDRs covers the node this pod lands on, the kubelet's probes are dropped
|
|
and THE POD NEVER BECOMES READY — the rollout then times out and the symptom
|
|
looks like a broken application. Compare
|
|
|
|
kubectl -n {{ .Release.Namespace }} describe pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-deployment
|
|
|
|
against `kubectl get node -o wide`.
|
|
{{- else }}
|
|
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
|
|
empty, so that rule carries no source selector at all and any pod the CNI
|
|
routes to can reach it. Not from the internet — but this is NOT pod-level
|
|
isolation. Narrowing it means listing the worker node IPs in
|
|
networkPolicy.probeSources, because the kubelet probes from the node's own
|
|
address and the chart cannot know it; a wrong guess there leaves the pod
|
|
permanently un-Ready.
|
|
{{- end }}
|
|
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
|
|
this object is accepted and filters nothing, so everything reads as
|
|
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
|
|
shows the object exists; only a connection attempt from an unrelated
|
|
namespace shows that it bites.
|
|
|
|
Switch it off with networkPolicy.enabled: false.
|
|
{{- end }}
|