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
28 changes: 26 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ jobs:
run: docker compose -f docker-compose.test.yaml down -v --remove-orphans || true

# ---------------------------------------------------------------------------
# Manifest validation — verify k8s YAML is valid against the k8s schema
# Manifest validation — verify static k8s/ YAML and the rendered Helm chart
# are valid against the k8s schema
# ---------------------------------------------------------------------------
manifests:
name: Validate k8s manifests
Expand All @@ -132,6 +133,29 @@ jobs:
curl -sSL https://github.com/yannh/kubeconform/releases/download/v0.7.0/kubeconform-linux-amd64.tar.gz \
| tar xz -C /usr/local/bin kubeconform

- name: Validate k8s manifests
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.16.2

- name: Validate static k8s manifests
run: |
kubeconform -strict -summary k8s/*.yaml

- name: Lint Helm chart
run: |
helm lint charts/floodgate

- name: Validate rendered chart (defaults)
run: |
helm template floodgate charts/floodgate \
| kubeconform -strict -summary

- name: Validate rendered chart (PDB enabled, drop enabled, json logs)
run: |
helm template floodgate charts/floodgate \
--set podDisruptionBudget.enabled=true \
--set config.drop_enabled=true \
--set 'config.drop_portnums={RANGE_TEST_APP}' \
--set config.log_format=json \
| kubeconform -strict -summary
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Gateway → EMQX → [ExHook gRPC] → floodgate → drop / modify / passthru
| `docker-compose.test.yaml` | Integration test stack (emqx, floodgate, exhook-init, test-driver) on an isolated bridge network. |
| `scripts/run-integration.sh` | Integration harness orchestrator — `--keep` leaves the stack up, `--teardown` removes it. |
| `tests/integration/` | Integration test assets: floodgate config, ExHook init container, test-driver image + cases. |
| `charts/floodgate/` | Helm chart (supported install path). Values mirror `config.yaml` 1:1; rendered chart is validated in CI. |
| `k8s/` | Static `kubectl apply -f` manifests, deprecated in favour of the chart. |

## Dev Setup

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,15 @@ docker compose up --build -d

After startup, register the ExHook per the [Deployment](#deployment) instructions above.

### Kubernetes
### Kubernetes (Helm)

```bash
helm install floodgate ./charts/floodgate -n floodgate --create-namespace
```

See [charts/floodgate/](charts/floodgate/) for the full values reference. The chart renders a Deployment with rolling updates, a ClusterIP Service, ConfigMap, ServiceAccount, and an optional PodDisruptionBudget; ConfigMap changes trigger automatic rolling restarts via a checksum annotation. Register the ExHook at `http://floodgate.floodgate.svc:9000` after install.

See [k8s/](k8s/) — Deployment, Service, and ConfigMap. The Deployment uses a rolling update strategy for zero-downtime upgrades. Register the ExHook at `http://floodgate:9000` after applying.
The flat manifests in [k8s/](k8s/) are still present for `kubectl apply -f` users but are deprecated in favour of the chart.

### Source install

Expand Down Expand Up @@ -211,7 +217,7 @@ The Kubernetes manifests in [k8s/](k8s/) use `type: ClusterIP` so neither port i

### Container hardening

The production container image runs as `nobody` (UID 65534) with a read-only filesystem, no Linux capabilities, and no privilege escalation. See [Dockerfile](Dockerfile) and [k8s/deployment.yaml](k8s/deployment.yaml).
The production container image runs as `nobody` (UID 65534) with a read-only filesystem, no Linux capabilities, and no privilege escalation. See [Dockerfile](Dockerfile) and the chart's [containerSecurityContext defaults](charts/floodgate/values.yaml).

## Verifying operation

Expand Down
12 changes: 12 additions & 0 deletions charts/floodgate/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Patterns to ignore when packaging the chart.
.DS_Store
.git/
.gitignore
.idea/
.vscode/
*.swp
*.bak
*.tmp
*.orig
*~
README.md.tpl
17 changes: 17 additions & 0 deletions charts/floodgate/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v2
name: floodgate
description: MQTT anti-flood service for Meshtastic — zero-hop modifier and portnum drop filter for EMQX via the ExHook gRPC interface.
type: application
version: 0.1.0
appVersion: "0.1.0"
home: https://github.com/eric-becker/floodgate
sources:
- https://github.com/eric-becker/floodgate
keywords:
- meshtastic
- mqtt
- emqx
- exhook
maintainers:
- name: Eric Becker
url: https://github.com/eric-becker
74 changes: 74 additions & 0 deletions charts/floodgate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# floodgate

Helm chart for [floodgate](https://github.com/eric-becker/floodgate), an MQTT anti-flood service for Meshtastic. Floodgate plugs into EMQX via the ExHook gRPC interface and either zeros the `hop_limit` of in-flight `MeshPacket`s or drops them outright by portnum.

## Install

```bash
# Default: standard public-channel zerohop, drop disabled, text logs
helm install floodgate ./charts/floodgate -n floodgate --create-namespace

# Common: enable RANGE_TEST_APP drop, JSON logs for Loki
helm install floodgate ./charts/floodgate -n floodgate --create-namespace \
--set config.drop_enabled=true \
--set 'config.drop_portnums={RANGE_TEST_APP}' \
--set config.log_format=json
```

After install, register the ExHook on EMQX:

```bash
curl -u admin:public -X POST http://<emqx-host>:18083/api/v5/exhooks \
-H 'Content-Type: application/json' \
-d '{
"name": "floodgate",
"url": "http://floodgate.floodgate.svc:9000",
"enable": true
}'
```

## Upgrade

```bash
helm upgrade floodgate ./charts/floodgate -n floodgate -f my-values.yaml
```

ConfigMap changes trigger a rolling restart automatically — the Deployment carries a `checksum/config` annotation that the chart recomputes from the rendered ConfigMap on every render.

## Uninstall

```bash
helm uninstall floodgate -n floodgate
```

## Values

The full default set lives in [values.yaml](values.yaml). Highlights:

| Key | Default | Notes |
|-----|---------|-------|
| `image.repository` | `ghcr.io/eric-becker/floodgate` | Image registry/repo |
| `image.tag` | `""` (Chart `appVersion`) | Override to pin |
| `replicaCount` | `1` | Each replica registers as its own ExHook target if you front them with a Service per pod; the default cluster-internal Service round-robins |
| `config.zerohop_enabled` | `true` | Master switch for the zero-hop modifier |
| `config.zerohop_channels` | 8 standard presets | Channels whose packets get `hop_limit` zeroed |
| `config.drop_enabled` | `false` | Master switch for the drop filter |
| `config.drop_channels` | `"zerohop_channels"` | Inherits from zerohop, or a list, or `null` for all channels |
| `config.drop_portnums` | `[]` | E.g. `[RANGE_TEST_APP]` |
| `config.log_format` | `"text"` | Set `"json"` for Loki/Grafana |
| `service.type` | `ClusterIP` | gRPC port only; health is internal |
| `podDisruptionBudget.enabled` | `false` | Set `true` and tune `minAvailable` for HA topologies |
| `resources.requests` / `limits` | 50m/200m CPU, 64Mi/128Mi memory | Tuned for ~1 broker; raise for high-throughput EMQX clusters |
| `containerSecurityContext` | nonroot, read-only fs, drop ALL caps | Matches the upstream Dockerfile |

The `config.*` block is rendered verbatim into the ConfigMap and read by floodgate via `FLOODGATE_CONFIG=/etc/floodgate/config.yaml`. Any key floodgate accepts can be set there; see [config.yaml](../../config.yaml) and the project [README](../../README.md) for the full schema.

## Health probes

The container exposes `/health` on `config.health_port` (default `8080`). The Service does **not** expose this port — health is for cluster-internal probes only. To inspect manually, port-forward the Pod:

```bash
POD=$(kubectl get pod -n floodgate -l app.kubernetes.io/name=floodgate -o name | head -1)
kubectl port-forward -n floodgate $POD 8080:8080
curl http://localhost:8080/health
```
29 changes: 29 additions & 0 deletions charts/floodgate/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Floodgate is installed.

Release: {{ .Release.Name }}
Namespace: {{ .Release.Namespace }}
Image: {{ include "floodgate.image" . }}

The gRPC ExHook endpoint is available cluster-internally at:

http://{{ include "floodgate.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.config.grpc_port }}

Register it on your EMQX broker:

curl -u admin:public -X POST http://<emqx-host>:18083/api/v5/exhooks \
-H 'Content-Type: application/json' \
-d '{
"name": "floodgate",
"url": "http://{{ include "floodgate.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.config.grpc_port }}",
"enable": true
}'

Tail floodgate logs:

kubectl logs -n {{ .Release.Namespace }} deploy/{{ include "floodgate.fullname" . }} -f

Inspect /health (the Service exposes only gRPC, so port-forward the Pod):

POD=$(kubectl get pod -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "floodgate.name" . }} -o name | head -1)
kubectl port-forward -n {{ .Release.Namespace }} $POD {{ .Values.config.health_port }}:{{ .Values.config.health_port }}
curl http://localhost:{{ .Values.config.health_port }}/health
70 changes: 70 additions & 0 deletions charts/floodgate/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "floodgate.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Fully qualified app name. Truncated to 63 chars for label compatibility.
*/}}
{{- define "floodgate.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 }}

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

{{/*
Common labels applied to every object the chart owns.
*/}}
{{- define "floodgate.labels" -}}
helm.sh/chart: {{ include "floodgate.chart" . }}
{{ include "floodgate.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels — used for matchLabels in the Deployment and the Service
selector. Must be stable across upgrades.
*/}}
{{- define "floodgate.selectorLabels" -}}
app.kubernetes.io/name: {{ include "floodgate.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Resolve the ServiceAccount name to use.
*/}}
{{- define "floodgate.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "floodgate.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Resolve the container image reference. Falls back to .Chart.appVersion when
.Values.image.tag is empty.
*/}}
{{- define "floodgate.image" -}}
{{- $tag := default .Chart.AppVersion .Values.image.tag -}}
{{- printf "%s:%s" .Values.image.repository $tag -}}
{{- end }}
9 changes: 9 additions & 0 deletions charts/floodgate/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "floodgate.fullname" . }}-config
labels:
{{- include "floodgate.labels" . | nindent 4 }}
data:
config.yaml: |
{{ toYaml .Values.config | indent 4 }}
89 changes: 89 additions & 0 deletions charts/floodgate/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "floodgate.fullname" . }}
labels:
{{- include "floodgate.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
{{- toYaml .Values.updateStrategy | nindent 4 }}
selector:
matchLabels:
{{- include "floodgate.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "floodgate.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
# Roll the pods when the rendered config changes.
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "floodgate.serviceAccountName" . }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: floodgate
image: {{ include "floodgate.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: grpc
containerPort: {{ .Values.config.grpc_port }}
protocol: TCP
- name: health
containerPort: {{ .Values.config.health_port }}
protocol: TCP
env:
- name: FLOODGATE_CONFIG
value: /etc/floodgate/config.yaml
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: config
mountPath: /etc/floodgate
readOnly: true
livenessProbe:
httpGet:
path: /health
port: health
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
readinessProbe:
httpGet:
path: /health
port: health
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
volumes:
- name: config
configMap:
name: {{ include "floodgate.fullname" . }}-config
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
Loading
Loading