From 69f636edf78b2e983b83c5d3c7dc2f85b83fc0e7 Mon Sep 17 00:00:00 2001 From: Mark Garratt Date: Fri, 26 Jun 2026 13:24:56 +0100 Subject: [PATCH] feat(proton-bridge): metrics port, PodMonitor, Recreate, digest (0.2.0) Make the metrics exporter first-class so consumers don't need postRenderer / extraEnv workarounds: - declare a `metrics` containerPort (container.metricsPort, default 9154) and set CONTAINER_METRICS_PORT; default bridge.mode to grpc (serves the exporter API) - add an optional PodMonitor (podMonitor.enabled) - default update strategy to Recreate (single ReadWriteOnce volume; rolling updates deadlock on volume multi-attach) - support image.digest for immutable pinning (repo:tag@digest) - bump appVersion to 3.25.0 Co-Authored-By: Claude Opus 4.8 (1M context) --- charts/proton-bridge/Chart.yaml | 4 ++-- charts/proton-bridge/README.md | 24 ++++++++++++++++--- .../proton-bridge/templates/deployment.yaml | 9 ++++++- .../proton-bridge/templates/podmonitor.yaml | 19 +++++++++++++++ charts/proton-bridge/templates/secret.yaml | 1 + charts/proton-bridge/values.yaml | 22 ++++++++++++++++- 6 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 charts/proton-bridge/templates/podmonitor.yaml diff --git a/charts/proton-bridge/Chart.yaml b/charts/proton-bridge/Chart.yaml index a67a80d..864e10a 100644 --- a/charts/proton-bridge/Chart.yaml +++ b/charts/proton-bridge/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: proton-bridge description: Proton Mail Bridge deployment for in-cluster SMTP/IMAP access type: application -version: 0.1.8 -appVersion: "3.22.0" +version: 0.2.0 +appVersion: "3.25.0" diff --git a/charts/proton-bridge/README.md b/charts/proton-bridge/README.md index 2caa1d6..c5859c4 100644 --- a/charts/proton-bridge/README.md +++ b/charts/proton-bridge/README.md @@ -64,17 +64,32 @@ kubectl -n "$NAMESPACE" rollout status deploy/"$DEPLOYMENT" - Service type: `ClusterIP` - Service ports: SMTP `25`, IMAP `143` (mapped to container ports `1026`/`1144` by default) - Image tag: `latest` +- Bridge mode: `grpc` (serves mail plus the gRPC API the metrics exporter scrapes) +- Metrics: exporter on container port `9154`; `PodMonitor` off by default +- Update strategy: `Recreate` (single `ReadWriteOnce` volume) - PVC: enabled, `ReadWriteOnce`, `2Gi` +## Metrics + +The image runs a Prometheus exporter (container port `container.metricsPort`, +default `9154`) when `bridge.mode=grpc` (the default). Enable scraping with the +Prometheus Operator: + +```yaml +podMonitor: + enabled: true +``` + ## Required Runtime Configuration -The image requires the following environment variables: +The image requires the following environment variables (all set by the chart): - `PROTON_BRIDGE_SMTP_PORT` - `PROTON_BRIDGE_IMAP_PORT` - `PROTON_BRIDGE_HOST` - `CONTAINER_SMTP_PORT` - `CONTAINER_IMAP_PORT` +- `CONTAINER_METRICS_PORT` By default, the chart creates a Secret with values from `values.yaml`. Set `existingSecret` to reuse your own Secret. @@ -88,11 +103,14 @@ helm upgrade --install proton-bridge ./charts/proton-bridge Common overrides: -- `image.tag` +- `image.tag`, `image.digest` (pin by digest: referenced as `repo:tag@digest`) +- `bridge.mode` (`grpc` default, `noninteractive`, or `cli`) +- `strategy` (defaults to `Recreate`) +- `podMonitor.enabled`, `podMonitor.interval`, `podMonitor.labels` - `service.type` - `persistence.existingClaim` - `bridge.host`, `bridge.smtpPort`, `bridge.imapPort` -- `container.smtpPort`, `container.imapPort` +- `container.smtpPort`, `container.imapPort`, `container.metricsPort` - `container.enablePrivilegedPortBinding` - `containerSecurityContext` - `volumePermissions.enabled` diff --git a/charts/proton-bridge/templates/deployment.yaml b/charts/proton-bridge/templates/deployment.yaml index 75b389a..e378fd9 100644 --- a/charts/proton-bridge/templates/deployment.yaml +++ b/charts/proton-bridge/templates/deployment.yaml @@ -6,6 +6,10 @@ metadata: {{- include "proton-bridge.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} + {{- with .Values.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} selector: matchLabels: {{- include "proton-bridge.selectorLabels" . | nindent 6 }} @@ -69,7 +73,7 @@ spec: {{- end }} containers: - name: proton-bridge - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}{{ with .Values.image.digest }}@{{ . }}{{ end }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- $containerSecurityContext := deepCopy (.Values.containerSecurityContext | default dict) }} {{- if .Values.container.enablePrivilegedPortBinding }} @@ -101,6 +105,9 @@ spec: - name: imap containerPort: {{ $containerImapPort }} protocol: TCP + - name: metrics + containerPort: {{ int .Values.container.metricsPort }} + protocol: TCP {{- if .Values.persistence.enabled }} volumeMounts: - name: bridge-data diff --git a/charts/proton-bridge/templates/podmonitor.yaml b/charts/proton-bridge/templates/podmonitor.yaml new file mode 100644 index 0000000..d97cf4b --- /dev/null +++ b/charts/proton-bridge/templates/podmonitor.yaml @@ -0,0 +1,19 @@ +{{- if .Values.podMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: {{ include "proton-bridge.fullname" . }} + labels: + {{- include "proton-bridge.labels" . | nindent 4 }} + {{- with .Values.podMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "proton-bridge.selectorLabels" . | nindent 6 }} + podMetricsEndpoints: + - port: metrics + path: {{ .Values.podMonitor.path }} + interval: {{ .Values.podMonitor.interval }} +{{- end }} diff --git a/charts/proton-bridge/templates/secret.yaml b/charts/proton-bridge/templates/secret.yaml index 33eef78..58fe484 100644 --- a/charts/proton-bridge/templates/secret.yaml +++ b/charts/proton-bridge/templates/secret.yaml @@ -23,4 +23,5 @@ stringData: PROTON_BRIDGE_IMAP_PORT: {{ $bridgeImapPort | quote }} CONTAINER_SMTP_PORT: {{ $containerSmtpPort | quote }} CONTAINER_IMAP_PORT: {{ $containerImapPort | quote }} + CONTAINER_METRICS_PORT: {{ int .Values.container.metricsPort | quote }} {{- end }} diff --git a/charts/proton-bridge/values.yaml b/charts/proton-bridge/values.yaml index cddb711..90fa04a 100644 --- a/charts/proton-bridge/values.yaml +++ b/charts/proton-bridge/values.yaml @@ -3,8 +3,17 @@ replicaCount: 1 image: repository: ghcr.io/mgarratt/docker-images/proton-bridge tag: latest + # Pin by content digest (e.g. sha256:...). When set, the image is referenced as + # repository:tag@digest, so the tag stays readable but the digest is immutable + # (avoids stale pulls from a mirror that cached a re-pushed mutable tag). + digest: "" pullPolicy: IfNotPresent +# Deployment update strategy. Recreate by default: bridge uses a single +# ReadWriteOnce volume, so a rolling update would deadlock on volume multi-attach. +strategy: + type: Recreate + imagePullSecrets: [] nameOverride: "" fullnameOverride: "" @@ -29,14 +38,25 @@ bridge: host: "127.0.0.1" smtpPort: 1025 imapPort: 1143 - mode: noninteractive + # grpc serves mail plus the gRPC API the metrics exporter scrapes. + # Use noninteractive for a leaner run without metrics, or cli for first login. + mode: grpc container: smtpPort: 1026 imapPort: 1144 + metricsPort: 9154 # Set true to add NET_BIND_SERVICE capability so non-root process can bind <1024 (e.g. 25/143). enablePrivilegedPortBinding: false +# Prometheus Operator PodMonitor scraping the bridge metrics exporter. +podMonitor: + enabled: false + interval: 30s + path: /metrics + # Extra labels on the PodMonitor (e.g. to match a Prometheus ruleSelector). + labels: {} + # Pod-level security context defaults for the bridge image user (uid:gid 1000:1000). podSecurityContext: fsGroup: 1000