From 16d3841a05b86f42efd8700c391b281b12b24229 Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Wed, 22 Jul 2026 20:47:11 +0800 Subject: [PATCH 01/11] From 4885b51d5bd967ef4cb8933a8f23886031b03544 Mon Sep 17 00:00:00 2001 From: Jared Patterson Date: Wed, 22 Jul 2026 11:30:35 +1200 Subject: [PATCH 02/11] ci(helm): add initial Helm chart and CI workflow Add a templated Helm chart under helm/ for deploying Logflare to Kubernetes, along with a GitHub Actions workflow that gates chart changes on pull requests. The workflow runs only on PRs that touch helm/**, and it: - installs Helm and the helm-unittest plugin - lints the chart - runs the chart unit tests - renders the chart with default values via helm template - verifies the chart version in Chart.yaml has been bumped above main --- .github/workflows/helm-chart.yml | 60 ++++++++ helm/.helmignore | 23 ++++ helm/Chart.yaml | 24 ++++ helm/deployment_test.yaml | 9 ++ helm/templates/NOTES.txt | 35 +++++ helm/templates/_helpers.tpl | 62 +++++++++ helm/templates/deployment.yaml | 78 +++++++++++ helm/templates/hpa.yaml | 32 +++++ helm/templates/httproute.yaml | 38 +++++ helm/templates/ingress.yaml | 43 ++++++ helm/templates/service.yaml | 15 ++ helm/templates/serviceaccount.yaml | 13 ++ helm/templates/tests/test-connection.yaml | 15 ++ helm/values.yaml | 161 ++++++++++++++++++++++ 14 files changed, 608 insertions(+) create mode 100644 .github/workflows/helm-chart.yml create mode 100644 helm/.helmignore create mode 100644 helm/Chart.yaml create mode 100644 helm/deployment_test.yaml create mode 100644 helm/templates/NOTES.txt create mode 100644 helm/templates/_helpers.tpl create mode 100644 helm/templates/deployment.yaml create mode 100644 helm/templates/hpa.yaml create mode 100644 helm/templates/httproute.yaml create mode 100644 helm/templates/ingress.yaml create mode 100644 helm/templates/service.yaml create mode 100644 helm/templates/serviceaccount.yaml create mode 100644 helm/templates/tests/test-connection.yaml create mode 100644 helm/values.yaml diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml new file mode 100644 index 0000000000..8e39a16efa --- /dev/null +++ b/.github/workflows/helm-chart.yml @@ -0,0 +1,60 @@ +name: Helm Chart + +on: + pull_request: + branches: [main] + paths: + - "helm/**" + +permissions: + contents: read + +jobs: + test: + name: Lint, test and template + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v4 + + - name: Install helm-unittest plugin + run: helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest + + - name: Lint chart + run: helm lint helm + + - name: Run unit tests + run: helm unittest -f '*_test.yaml' helm + + - name: Template chart with default values + run: helm template helm + + - name: Ensure chart version has been bumped + run: | + head_version=$(helm show chart helm | awk '/^version:/ {print $2}') + base_version=$(git show "origin/${{ github.base_ref }}:helm/Chart.yaml" 2>/dev/null | awk '/^version:/ {print $2}') + + echo "Base (${{ github.base_ref }}) chart version: ${base_version:-}" + echo "Head chart version: $head_version" + + if [ -z "$base_version" ]; then + echo "No chart on base branch; treating as new chart." + exit 0 + fi + + if [ "$head_version" = "$base_version" ]; then + echo "::error::Chart version has not been bumped (still $head_version). Update version in helm/Chart.yaml." + exit 1 + fi + + highest=$(printf '%s\n%s\n' "$base_version" "$head_version" | sort -V | tail -n1) + if [ "$highest" != "$head_version" ]; then + echo "::error::Chart version $head_version is lower than base version $base_version." + exit 1 + fi + + echo "Chart version bumped: $base_version -> $head_version" diff --git a/helm/.helmignore b/helm/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000000..468970a642 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: logflare +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm/deployment_test.yaml b/helm/deployment_test.yaml new file mode 100644 index 0000000000..2262855cf2 --- /dev/null +++ b/helm/deployment_test.yaml @@ -0,0 +1,9 @@ +suite: test deployment kind +templates: + - deployment.yaml +tests: + - it: should render the correct deployment Kind + asserts: + - equal: + path: kind + value: Deployment diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt new file mode 100644 index 0000000000..e32d259175 --- /dev/null +++ b/helm/templates/NOTES.txt @@ -0,0 +1,35 @@ +1. Get the application URL by running these commands: +{{- if .Values.httpRoute.enabled }} +{{- if .Values.httpRoute.hostnames }} + export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} +{{- else }} + export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") + {{- end }} +{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} + echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" + + NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. + The rules can be set for path, method, header and query parameters. + You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' +{{- end }} +{{- else if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "logflare.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "logflare.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "logflare.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "logflare.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl new file mode 100644 index 0000000000..304c1221bb --- /dev/null +++ b/helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "logflare.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "logflare.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 }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "logflare.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "logflare.labels" -}} +helm.sh/chart: {{ include "logflare.chart" . }} +{{ include "logflare.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "logflare.selectorLabels" -}} +app.kubernetes.io/name: {{ include "logflare.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "logflare.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "logflare.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000000..8efa8952b4 --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "logflare.fullname" . }} + labels: + {{- include "logflare.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "logflare.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "logflare.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "logflare.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml new file mode 100644 index 0000000000..94f4379a8b --- /dev/null +++ b/helm/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "logflare.fullname" . }} + labels: + {{- include "logflare.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "logflare.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/helm/templates/httproute.yaml b/helm/templates/httproute.yaml new file mode 100644 index 0000000000..aa4e068682 --- /dev/null +++ b/helm/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "logflare.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "logflare.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml new file mode 100644 index 0000000000..7f8497f3d3 --- /dev/null +++ b/helm/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "logflare.fullname" . }} + labels: + {{- include "logflare.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "logflare.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000000..7a4cc73e39 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "logflare.fullname" . }} + labels: + {{- include "logflare.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "logflare.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml new file mode 100644 index 0000000000..b6ffe9006d --- /dev/null +++ b/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "logflare.serviceAccountName" . }} + labels: + {{- include "logflare.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..6ce4305366 --- /dev/null +++ b/helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "logflare.fullname" . }}-test-connection" + labels: + {{- include "logflare.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "logflare.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000000..489758f912 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,161 @@ +# Default values for logflare. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created. + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account. + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template. + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} From f08fa33baf7cd3f185d360cf5e858a9989c267ab Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Wed, 22 Jul 2026 21:27:13 +0800 Subject: [PATCH 03/11] feat: tweaks to helm chart for logflare deployment --- helm/Chart.yaml | 4 +- helm/README.md | 144 ++++++++++++++++++++++ helm/configmap_test.yaml | 75 +++++++++++ helm/deployment_test.yaml | 78 ++++++++++++ helm/templates/configmap.yaml | 41 ++++++ helm/templates/deployment.yaml | 30 ++++- helm/templates/secret.yaml | 21 ++++ helm/templates/service.yaml | 4 + helm/templates/tests/test-connection.yaml | 2 +- helm/values.yaml | 78 +++++++++++- 10 files changed, 467 insertions(+), 10 deletions(-) create mode 100644 helm/README.md create mode 100644 helm/configmap_test.yaml create mode 100644 helm/templates/configmap.yaml create mode 100644 helm/templates/secret.yaml diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 468970a642..bba6fb81aa 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: logflare -description: A Helm chart for Kubernetes +description: Deploys single-tenant Logflare, supporting BigQuery or Postgres as the event storage backend # A chart can be either an 'application' or a 'library' chart. # @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.16.0" +appVersion: "1.47.2" diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 0000000000..05129c18c5 --- /dev/null +++ b/helm/README.md @@ -0,0 +1,144 @@ +# Logflare Helm Chart + +Deploys single-tenant Logflare on Kubernetes, supporting either **BigQuery** or **Postgres** as the event storage backend. + +## Quick install + +```sh +helm install logflare ./helm -f my-values.yaml +``` + +At minimum, `my-values.yaml` must pick a backend and supply that backend's credentials (see below). + +## Configuring the backend + +Set `logflare.backend.type` to `bigquery` or `postgres`. These modes are mutually exclusive. + +**BigQuery:** + +```yaml +logflare: + backend: + type: bigquery + bigquery: + projectId: "my-gcp-project" + projectNumber: "1234567890" +``` + +The service account key itself is a secret — see [Loading secrets](#loading-secrets). + +**Postgres:** + +```yaml +logflare: + backend: + type: postgres + postgres: + schema: "public" +``` + +The connection URL (`POSTGRES_BACKEND_URL`) is a secret and is not set here — see below. + +## Loading secrets + +The chart needs several sensitive values: `LOGFLARE_PUBLIC_ACCESS_TOKEN`, `LOGFLARE_PRIVATE_ACCESS_TOKEN`, `DB_PASSWORD`, `LOGFLARE_DB_ENCRYPTION_KEY`, `PHX_SECRET_KEY_BASE`, `PHX_LIVE_VIEW_SIGNING_SALT`, and one of `POSTGRES_BACKEND_URL` (postgres backend) or `GOOGLE_SERVICE_ACCOUNT` (bigquery backend, the raw service-account JSON key content). The exact set is defined in `templates/secret.yaml`. + +There are three ways to supply them: + +### 1. Inline via values (quick/dev use) + +```yaml +logflare: + secrets: + publicAccessToken: "..." + privateAccessToken: "..." + dbPassword: "..." + dbEncryptionKey: "..." + phxSecretKeyBase: "..." # e.g. `openssl rand -base64 48` + phxLiveViewSigningSalt: "..." # e.g. `openssl rand -base64 8` + postgresBackendUrl: "postgresql://user:pass@host:5432/db" # postgres backend + # googleServiceAccountJson: '{"type": "service_account", ...}' # bigquery backend +``` + +Keep this in a values file that is *not* committed to source control (e.g. `secrets-values.yaml`), and pass it alongside your other values: + +```sh +helm install logflare ./helm -f my-values.yaml -f secrets-values.yaml +``` + +This is the simplest option, but the values end up stored in the Helm release's state (in-cluster), which is not ideal for production. + +### 2. `existingSecret` (recommended for production) + +Create the Secret yourself, outside of Helm, then point the chart at it: + +```sh +kubectl create secret generic logflare-secrets \ + --from-literal=LOGFLARE_PUBLIC_ACCESS_TOKEN=... \ + --from-literal=LOGFLARE_PRIVATE_ACCESS_TOKEN=... \ + --from-literal=DB_PASSWORD=... \ + --from-literal=LOGFLARE_DB_ENCRYPTION_KEY=... \ + --from-literal=PHX_SECRET_KEY_BASE=... \ + --from-literal=PHX_LIVE_VIEW_SIGNING_SALT=... \ + --from-literal=POSTGRES_BACKEND_URL=postgresql://user:pass@host:5432/db + # or: --from-file=GOOGLE_SERVICE_ACCOUNT=./gcloud.json (bigquery backend) +``` + +```yaml +logflare: + existingSecret: "logflare-secrets" +``` + +When `existingSecret` is set, the chart does not create its own Secret (`templates/secret.yaml` renders nothing) and instead references the named Secret directly in the Deployment's `envFrom`. The Secret must contain the same keys the chart would otherwise generate — see `templates/secret.yaml` for the authoritative list per backend type. + +### 3. External secret managers + +Tools like Sealed Secrets or the External Secrets Operator work out of the box with this chart: point `logflare.existingSecret` at whatever Secret name your tool ultimately produces in-cluster. No chart changes are needed. + +## Configurable values + +| `values.yaml` key | Env var | Notes | +|---|---|---| +| `logflare.singleTenant` | `LOGFLARE_SINGLE_TENANT` | | +| `logflare.supabaseMode` | `LOGFLARE_SUPABASE_MODE` | | +| `logflare.nodeHost` | `LOGFLARE_NODE_HOST` | When empty (default), the chart injects the pod IP via the downward API instead — needed for clustering, since a fixed value would collide across pods | +| `logflare.grpcPort` | `LOGFLARE_GRPC_PORT` | | +| `logflare.httpConnectionPools` | `LOGFLARE_HTTP_CONNECTION_POOLS` | | +| `logflare.featureFlagOverride` | `LOGFLARE_FEATURE_FLAG_OVERRIDE` | | +| `logflare.phx.httpPort` | `PHX_HTTP_PORT` | | +| `logflare.phx.urlHost` | `PHX_URL_HOST` | | +| `logflare.phx.urlScheme` | `PHX_URL_SCHEME` | | +| `logflare.phx.urlPort` | `PHX_URL_PORT` | | +| `logflare.phx.checkOrigin` | `PHX_CHECK_ORIGIN` | | +| `logflare.db.hostname` | `DB_HOSTNAME` | Logflare's own metadata Postgres, distinct from the postgres event backend | +| `logflare.db.port` | `DB_PORT` | | +| `logflare.db.database` | `DB_DATABASE` | | +| `logflare.db.username` | `DB_USERNAME` | | +| `logflare.db.schema` | `DB_SCHEMA` | | +| `logflare.db.poolSize` | `DB_POOL_SIZE` | | +| `logflare.db.ssl` | `DB_SSL` | | +| `logflare.backend.type` | — | `bigquery` or `postgres`, selects which of the two blocks below is rendered | +| `logflare.backend.bigquery.projectId` | `GOOGLE_PROJECT_ID` | bigquery only | +| `logflare.backend.bigquery.projectNumber` | `GOOGLE_PROJECT_NUMBER` | bigquery only | +| `logflare.backend.bigquery.datasetIdAppend` | `GOOGLE_DATASET_ID_APPEND` | bigquery only | +| `logflare.backend.bigquery.datasetLocation` | `GOOGLE_DATASET_LOCATION` | bigquery only | +| `logflare.backend.postgres.schema` | `POSTGRES_BACKEND_SCHEMA` | postgres only | +| `logflare.secrets.*` / `logflare.existingSecret` | see [Loading secrets](#loading-secrets) | | + +See `values.yaml` for the full set of generic chart values (image, service, ingress, resources, autoscaling, etc.). + +## Verifying a deployment + +Render the manifests locally before installing: + +```sh +helm template logflare ./helm -f my-values.yaml -f secrets-values.yaml +``` + +Or dry-run against a real cluster: + +```sh +helm install logflare ./helm -f my-values.yaml --dry-run +``` + +The container's liveness and readiness probes hit `/health` on the service port (default `4000`). diff --git a/helm/configmap_test.yaml b/helm/configmap_test.yaml new file mode 100644 index 0000000000..3202c2e418 --- /dev/null +++ b/helm/configmap_test.yaml @@ -0,0 +1,75 @@ +suite: test backend wiring +templates: + - configmap.yaml + - secret.yaml +tests: + - it: should render bigquery vars and omit postgres vars by default + asserts: + - equal: + path: data.GOOGLE_PROJECT_ID + value: "" + template: configmap.yaml + - isNotNull: + path: data.GOOGLE_DATASET_LOCATION + template: configmap.yaml + - isNull: + path: data.POSTGRES_BACKEND_SCHEMA + template: configmap.yaml + - isNotNull: + path: stringData.GOOGLE_SERVICE_ACCOUNT + template: secret.yaml + - isNull: + path: stringData.POSTGRES_BACKEND_URL + template: secret.yaml + + - it: should render postgres vars and omit bigquery vars when backend.type is postgres + set: + logflare.backend.type: postgres + asserts: + - isNotNull: + path: data.POSTGRES_BACKEND_SCHEMA + template: configmap.yaml + - isNull: + path: data.GOOGLE_PROJECT_ID + template: configmap.yaml + - isNull: + path: data.GOOGLE_PROJECT_NUMBER + template: configmap.yaml + - isNotNull: + path: stringData.POSTGRES_BACKEND_URL + template: secret.yaml + - isNull: + path: stringData.GOOGLE_SERVICE_ACCOUNT + template: secret.yaml + + - it: should always render the phoenix session secrets + asserts: + - isNotNull: + path: stringData.PHX_SECRET_KEY_BASE + template: secret.yaml + - isNotNull: + path: stringData.PHX_LIVE_VIEW_SIGNING_SALT + template: secret.yaml + + - it: should omit empty optional configmap keys + asserts: + - notExists: + path: data.LOGFLARE_FEATURE_FLAG_OVERRIDE + template: configmap.yaml + - notExists: + path: data.PHX_URL_HOST + template: configmap.yaml + + - it: should render optional configmap keys when set + set: + logflare.featureFlagOverride: "some-flag" + logflare.phx.urlHost: "example.com" + asserts: + - equal: + path: data.LOGFLARE_FEATURE_FLAG_OVERRIDE + value: "some-flag" + template: configmap.yaml + - equal: + path: data.PHX_URL_HOST + value: "example.com" + template: configmap.yaml diff --git a/helm/deployment_test.yaml b/helm/deployment_test.yaml index 2262855cf2..b10a45d8ec 100644 --- a/helm/deployment_test.yaml +++ b/helm/deployment_test.yaml @@ -1,9 +1,87 @@ suite: test deployment kind templates: - deployment.yaml + - configmap.yaml + - secret.yaml tests: - it: should render the correct deployment Kind asserts: - equal: path: kind value: Deployment + template: deployment.yaml + + - it: should reference the chart's ConfigMap and Secret via envFrom by default + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom[0].configMapRef.name + value: RELEASE-NAME-logflare + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].envFrom[1].secretRef.name + value: RELEASE-NAME-logflare + template: deployment.yaml + + - it: should reference an existingSecret when set, instead of the chart's own Secret + set: + logflare.existingSecret: my-custom-secret + asserts: + - equal: + path: spec.template.spec.containers[0].envFrom[1].secretRef.name + value: my-custom-secret + template: deployment.yaml + + - it: should render checksum/config annotation, and checksum/secret only when not using existingSecret + asserts: + - isNotNull: + path: spec.template.metadata.annotations["checksum/config"] + template: deployment.yaml + - isNotNull: + path: spec.template.metadata.annotations["checksum/secret"] + template: deployment.yaml + + - it: should omit checksum/secret annotation when using an existingSecret + set: + logflare.existingSecret: my-custom-secret + asserts: + - isNotNull: + path: spec.template.metadata.annotations["checksum/config"] + template: deployment.yaml + - notExists: + path: spec.template.metadata.annotations["checksum/secret"] + template: deployment.yaml + + - it: should inject LOGFLARE_NODE_HOST from the pod IP by default + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].name + value: LOGFLARE_NODE_HOST + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[0].valueFrom.fieldRef.fieldPath + value: status.podIP + template: deployment.yaml + + - it: should use an explicit LOGFLARE_NODE_HOST when set + set: + logflare.nodeHost: "my-host" + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].name + value: LOGFLARE_NODE_HOST + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[0].value + value: "my-host" + template: deployment.yaml + + - it: should expose http and grpc container ports + asserts: + - equal: + path: spec.template.spec.containers[0].ports[0].containerPort + value: 4000 + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].ports[1].containerPort + value: 50051 + template: deployment.yaml diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml new file mode 100644 index 0000000000..89c39dfdd1 --- /dev/null +++ b/helm/templates/configmap.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "logflare.fullname" . }} + labels: + {{- include "logflare.labels" . | nindent 4 }} +data: + LOGFLARE_SINGLE_TENANT: {{ .Values.logflare.singleTenant | quote }} + LOGFLARE_SUPABASE_MODE: {{ .Values.logflare.supabaseMode | quote }} + LOGFLARE_GRPC_PORT: {{ .Values.logflare.grpcPort | quote }} + LOGFLARE_HTTP_CONNECTION_POOLS: {{ .Values.logflare.httpConnectionPools | quote }} + {{- if .Values.logflare.featureFlagOverride }} + LOGFLARE_FEATURE_FLAG_OVERRIDE: {{ .Values.logflare.featureFlagOverride | quote }} + {{- end }} + + PHX_HTTP_PORT: {{ .Values.logflare.phx.httpPort | quote }} + {{- if .Values.logflare.phx.urlHost }} + PHX_URL_HOST: {{ .Values.logflare.phx.urlHost | quote }} + {{- end }} + PHX_URL_SCHEME: {{ .Values.logflare.phx.urlScheme | quote }} + PHX_URL_PORT: {{ .Values.logflare.phx.urlPort | quote }} + PHX_CHECK_ORIGIN: {{ .Values.logflare.phx.checkOrigin | quote }} + + DB_HOSTNAME: {{ .Values.logflare.db.hostname | quote }} + DB_PORT: {{ .Values.logflare.db.port | quote }} + DB_DATABASE: {{ .Values.logflare.db.database | quote }} + DB_USERNAME: {{ .Values.logflare.db.username | quote }} + DB_SCHEMA: {{ .Values.logflare.db.schema | quote }} + DB_POOL_SIZE: {{ .Values.logflare.db.poolSize | quote }} + DB_SSL: {{ .Values.logflare.db.ssl | quote }} + + {{- if eq .Values.logflare.backend.type "bigquery" }} + GOOGLE_PROJECT_ID: {{ .Values.logflare.backend.bigquery.projectId | quote }} + GOOGLE_PROJECT_NUMBER: {{ .Values.logflare.backend.bigquery.projectNumber | quote }} + GOOGLE_DATASET_ID_APPEND: {{ .Values.logflare.backend.bigquery.datasetIdAppend | quote }} + GOOGLE_DATASET_LOCATION: {{ .Values.logflare.backend.bigquery.datasetLocation | quote }} + {{- else if eq .Values.logflare.backend.type "postgres" }} + POSTGRES_BACKEND_SCHEMA: {{ .Values.logflare.backend.postgres.schema | quote }} + {{- else }} + {{- fail (printf "logflare.backend.type must be 'bigquery' or 'postgres', got %q" .Values.logflare.backend.type) }} + {{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 8efa8952b4..d6b9b85e6a 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -13,10 +13,14 @@ spec: {{- include "logflare.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if not .Values.logflare.existingSecret }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} labels: {{- include "logflare.labels" . | nindent 8 }} {{- with .Values.podLabels }} @@ -44,6 +48,24 @@ spec: - name: http containerPort: {{ .Values.service.port }} protocol: TCP + - name: grpc + containerPort: {{ .Values.logflare.grpcPort }} + protocol: TCP + env: + {{- if .Values.logflare.nodeHost }} + - name: LOGFLARE_NODE_HOST + value: {{ .Values.logflare.nodeHost | quote }} + {{- else }} + - name: LOGFLARE_NODE_HOST + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- end }} + envFrom: + - configMapRef: + name: {{ include "logflare.fullname" . }} + - secretRef: + name: {{ .Values.logflare.existingSecret | default (include "logflare.fullname" .) }} {{- with .Values.livenessProbe }} livenessProbe: {{- toYaml . | nindent 12 }} @@ -52,6 +74,10 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.resources }} resources: {{- toYaml . | nindent 12 }} diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml new file mode 100644 index 0000000000..c2685b748d --- /dev/null +++ b/helm/templates/secret.yaml @@ -0,0 +1,21 @@ +{{- if not .Values.logflare.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "logflare.fullname" . }} + labels: + {{- include "logflare.labels" . | nindent 4 }} +type: Opaque +stringData: + LOGFLARE_PUBLIC_ACCESS_TOKEN: {{ .Values.logflare.secrets.publicAccessToken | quote }} + LOGFLARE_PRIVATE_ACCESS_TOKEN: {{ .Values.logflare.secrets.privateAccessToken | quote }} + DB_PASSWORD: {{ .Values.logflare.secrets.dbPassword | quote }} + LOGFLARE_DB_ENCRYPTION_KEY: {{ .Values.logflare.secrets.dbEncryptionKey | quote }} + PHX_SECRET_KEY_BASE: {{ .Values.logflare.secrets.phxSecretKeyBase | quote }} + PHX_LIVE_VIEW_SIGNING_SALT: {{ .Values.logflare.secrets.phxLiveViewSigningSalt | quote }} + {{- if eq .Values.logflare.backend.type "postgres" }} + POSTGRES_BACKEND_URL: {{ .Values.logflare.secrets.postgresBackendUrl | quote }} + {{- else if eq .Values.logflare.backend.type "bigquery" }} + GOOGLE_SERVICE_ACCOUNT: {{ .Values.logflare.secrets.googleServiceAccountJson | quote }} + {{- end }} +{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml index 7a4cc73e39..46ae8b90f9 100644 --- a/helm/templates/service.yaml +++ b/helm/templates/service.yaml @@ -11,5 +11,9 @@ spec: targetPort: http protocol: TCP name: http + - port: {{ .Values.logflare.grpcPort }} + targetPort: grpc + protocol: TCP + name: grpc selector: {{- include "logflare.selectorLabels" . | nindent 4 }} diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml index 6ce4305366..5da1f18d9e 100644 --- a/helm/templates/tests/test-connection.yaml +++ b/helm/templates/tests/test-connection.yaml @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "logflare.fullname" . }}:{{ .Values.service.port }}'] + args: ['{{ include "logflare.fullname" . }}:{{ .Values.service.port }}/health'] restartPolicy: Never diff --git a/helm/values.yaml b/helm/values.yaml index 489758f912..5def8fe008 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -7,12 +7,73 @@ replicaCount: 1 # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ image: - repository: nginx + repository: supabase/logflare # This sets the pull policy for images. pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" +# Logflare single-tenant application configuration. +# These values are rendered into a ConfigMap (non-sensitive) and a Secret +# (sensitive) which are wired into the Deployment via envFrom. +logflare: + singleTenant: true + supabaseMode: false + + # Erlang node host, used to build the node name for clustering. When empty, + # the chart injects the pod IP via the downward API instead of a fixed value. + nodeHost: "" + grpcPort: 50051 + httpConnectionPools: "all" + featureFlagOverride: "" + + phx: + httpPort: 4000 + urlHost: "" + urlScheme: "https" + urlPort: 443 + checkOrigin: false + + # Sensitive values. Either set them inline here (rendered into a Secret + # this chart creates), or set existingSecret to the name of a Secret you + # manage yourself containing the same keys (see templates/secret.yaml). + existingSecret: "" + secrets: + publicAccessToken: "" + privateAccessToken: "" + dbPassword: "" + dbEncryptionKey: "" + # Phoenix session/cookie signing. Required for the app to boot. + phxSecretKeyBase: "" + phxLiveViewSigningSalt: "" + # Only used when backend.type == postgres + postgresBackendUrl: "" + # Only used when backend.type == bigquery. Contents of the GCP service + # account JSON key file. + googleServiceAccountJson: "" + + # Logflare's own metadata Postgres database (distinct from the optional + # "postgres" event-storage backend below). + db: + hostname: "" + port: 5432 + database: "logflare" + username: "postgres" + schema: "public" + poolSize: 10 + ssl: false + + # Event storage backend. bigquery and postgres are mutually exclusive. + backend: + type: bigquery # bigquery | postgres + bigquery: + projectId: "" + projectNumber: "" + datasetIdAppend: "_prod" + datasetLocation: "US" + postgres: + schema: "public" # POSTGRES_BACKEND_SCHEMA; url comes from logflare.secrets.postgresBackendUrl + # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ imagePullSecrets: [] # This is to override the chart name. @@ -54,7 +115,7 @@ service: # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types type: ClusterIP # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports - port: 80 + port: 4000 # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ ingress: @@ -94,7 +155,7 @@ httpRoute: - matches: - path: type: PathPrefix - value: /headers + value: / # filters: # - type: RequestHeaderModifier # requestHeaderModifier: @@ -126,12 +187,19 @@ resources: {} # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ livenessProbe: httpGet: - path: / + path: /health port: http readinessProbe: httpGet: - path: / + path: /health + port: http +# Guards against liveness/readiness killing the pod mid-migration on first boot. +startupProbe: + httpGet: + path: /health port: http + failureThreshold: 30 + periodSeconds: 10 # This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ autoscaling: From 98bd1bf29c517dbb4e7046f2c93e738650427262 Mon Sep 17 00:00:00 2001 From: Ziinc Date: Thu, 23 Jul 2026 15:35:44 +0800 Subject: [PATCH 04/11] chore: upate to recommend ESO --- helm/README.md | 149 +++++++++++++++++++++++++++------ helm/configmap_test.yaml | 11 +++ helm/deployment_test.yaml | 86 +++++++++++++++---- helm/templates/_helpers.tpl | 15 ++++ helm/templates/deployment.yaml | 15 +++- helm/templates/secret.yaml | 34 +++++--- helm/values.yaml | 56 +++++++++---- 7 files changed, 297 insertions(+), 69 deletions(-) diff --git a/helm/README.md b/helm/README.md index 05129c18c5..ad62cf406e 100644 --- a/helm/README.md +++ b/helm/README.md @@ -41,23 +41,53 @@ The connection URL (`POSTGRES_BACKEND_URL`) is a secret and is not set here — ## Loading secrets -The chart needs several sensitive values: `LOGFLARE_PUBLIC_ACCESS_TOKEN`, `LOGFLARE_PRIVATE_ACCESS_TOKEN`, `DB_PASSWORD`, `LOGFLARE_DB_ENCRYPTION_KEY`, `PHX_SECRET_KEY_BASE`, `PHX_LIVE_VIEW_SIGNING_SALT`, and one of `POSTGRES_BACKEND_URL` (postgres backend) or `GOOGLE_SERVICE_ACCOUNT` (bigquery backend, the raw service-account JSON key content). The exact set is defined in `templates/secret.yaml`. +The chart needs several sensitive values, each configured under its own key in `logflare.secrets.*`: -There are three ways to supply them: +| `logflare.secrets.*` key | Env var | Notes | +|---|---|---| +| `publicAccessToken` | `LOGFLARE_PUBLIC_ACCESS_TOKEN` | | +| `privateAccessToken` | `LOGFLARE_PRIVATE_ACCESS_TOKEN` | | +| `dbPassword` | `DB_PASSWORD` | | +| `dbEncryptionKey` | `LOGFLARE_DB_ENCRYPTION_KEY` | | +| `phxSecretKeyBase` | `PHX_SECRET_KEY_BASE` | e.g. `openssl rand -base64 48` | +| `phxLiveViewSigningSalt` | `PHX_LIVE_VIEW_SIGNING_SALT` | e.g. `openssl rand -base64 8` | +| `postgresBackendUrl` | `POSTGRES_BACKEND_URL` | postgres backend only | +| `googleServiceAccountJson` | `GOOGLE_SERVICE_ACCOUNT` | bigquery backend only, the raw service-account JSON key content | + +Each field is its own object with three sub-keys: + +```yaml +logflare: + secrets: + : + value: "" # inline value, rendered into the chart's own Secret + existingSecret: "" # OR: name of a Secret that already exists in the namespace + existingSecretKey: "" # key within that Secret (defaults to the chart's own key name above) +``` + +For each field independently: if `existingSecret` is empty, the chart renders that key into the Secret it creates (named after the release) using `value`. If `existingSecret` is set, the chart instead wires that env var via `secretKeyRef` straight to `existingSecret`/`existingSecretKey`, and **does not** render that key into its own Secret. You can mix both approaches — e.g. keep `dbPassword` inline while sourcing `googleServiceAccountJson` from an externally managed Secret. ### 1. Inline via values (quick/dev use) ```yaml logflare: secrets: - publicAccessToken: "..." - privateAccessToken: "..." - dbPassword: "..." - dbEncryptionKey: "..." - phxSecretKeyBase: "..." # e.g. `openssl rand -base64 48` - phxLiveViewSigningSalt: "..." # e.g. `openssl rand -base64 8` - postgresBackendUrl: "postgresql://user:pass@host:5432/db" # postgres backend - # googleServiceAccountJson: '{"type": "service_account", ...}' # bigquery backend + publicAccessToken: + value: "..." + privateAccessToken: + value: "..." + dbPassword: + value: "..." + dbEncryptionKey: + value: "..." + phxSecretKeyBase: + value: "..." # e.g. `openssl rand -base64 48` + phxLiveViewSigningSalt: + value: "..." # e.g. `openssl rand -base64 8` + postgresBackendUrl: + value: "postgresql://user:pass@host:5432/db" # postgres backend + # googleServiceAccountJson: + # value: '{"type": "service_account", ...}' # bigquery backend ``` Keep this in a values file that is *not* committed to source control (e.g. `secrets-values.yaml`), and pass it alongside your other values: @@ -68,32 +98,97 @@ helm install logflare ./helm -f my-values.yaml -f secrets-values.yaml This is the simplest option, but the values end up stored in the Helm release's state (in-cluster), which is not ideal for production. -### 2. `existingSecret` (recommended for production) +### 2. Referencing an existing Secret (recommended for production) -Create the Secret yourself, outside of Helm, then point the chart at it: +Create the Secret yourself, outside of Helm, then point the relevant field(s) at it via `existingSecret`/`existingSecretKey`. Unlike the previous whole-chart `existingSecret` toggle, this is per field — the Secret's key names don't need to match the chart's own, and different fields can point at entirely different Secrets: ```sh kubectl create secret generic logflare-secrets \ - --from-literal=LOGFLARE_PUBLIC_ACCESS_TOKEN=... \ - --from-literal=LOGFLARE_PRIVATE_ACCESS_TOKEN=... \ - --from-literal=DB_PASSWORD=... \ - --from-literal=LOGFLARE_DB_ENCRYPTION_KEY=... \ - --from-literal=PHX_SECRET_KEY_BASE=... \ - --from-literal=PHX_LIVE_VIEW_SIGNING_SALT=... \ - --from-literal=POSTGRES_BACKEND_URL=postgresql://user:pass@host:5432/db - # or: --from-file=GOOGLE_SERVICE_ACCOUNT=./gcloud.json (bigquery backend) + --from-literal=public-access-token=... \ + --from-literal=private-access-token=... \ + --from-literal=db-password=... ``` ```yaml logflare: - existingSecret: "logflare-secrets" + secrets: + publicAccessToken: + existingSecret: "logflare-secrets" + existingSecretKey: "public-access-token" + privateAccessToken: + existingSecret: "logflare-secrets" + existingSecretKey: "private-access-token" + dbPassword: + existingSecret: "logflare-secrets" + existingSecretKey: "db-password" + # remaining fields fall back to `value` / the chart's own Secret ``` -When `existingSecret` is set, the chart does not create its own Secret (`templates/secret.yaml` renders nothing) and instead references the named Secret directly in the Deployment's `envFrom`. The Secret must contain the same keys the chart would otherwise generate — see `templates/secret.yaml` for the authoritative list per backend type. - -### 3. External secret managers - -Tools like Sealed Secrets or the External Secrets Operator work out of the box with this chart: point `logflare.existingSecret` at whatever Secret name your tool ultimately produces in-cluster. No chart changes are needed. +### 3. External Secrets Operator (ESO) + +[External Secrets Operator](https://external-secrets.io/) is the recommended way to run this in production: it syncs values from a real secret manager (AWS Secrets Manager, GCP Secret Manager, Vault, etc.) into a plain Kubernetes `Secret` that this chart then references via `existingSecret`. + +1. **Provision an `ExternalSecret`** (outside this chart, e.g. in your cluster-config repo) that materializes the Secret this chart will consume: + + ```yaml + apiVersion: external-secrets.io/v1 + kind: ExternalSecret + metadata: + name: logflare-secrets + namespace: my-namespace + spec: + secretStoreRef: + kind: ClusterSecretStore + name: aws-secrets-store + refreshPolicy: Periodic + refreshInterval: 1h + target: + name: logflare-secrets + creationPolicy: Owner + deletionPolicy: Delete + dataFrom: + - extract: + key: prod/logflare/secrets + ``` + + This produces a Secret named `logflare-secrets` in the namespace, with whatever keys exist at `prod/logflare/secrets` in your secret store (e.g. `public_access_token`, `private_access_token`, `db_password`, ...). ESO owns creation/rotation of this Secret entirely; the chart never sees the underlying values. + +2. **Point the chart's fields at it**, matching each field to the key your secret store uses: + + ```yaml + logflare: + secrets: + publicAccessToken: + existingSecret: "logflare-secrets" + existingSecretKey: "public_access_token" + privateAccessToken: + existingSecret: "logflare-secrets" + existingSecretKey: "private_access_token" + dbPassword: + existingSecret: "logflare-secrets" + existingSecretKey: "db_password" + dbEncryptionKey: + existingSecret: "logflare-secrets" + existingSecretKey: "db_encryption_key" + phxSecretKeyBase: + existingSecret: "logflare-secrets" + existingSecretKey: "phx_secret_key_base" + phxLiveViewSigningSalt: + existingSecret: "logflare-secrets" + existingSecretKey: "phx_live_view_signing_salt" + # bigquery backend: + googleServiceAccountJson: + existingSecret: "logflare-secrets" + existingSecretKey: "google_service_account_json" + ``` + +3. **Install/upgrade as usual** — no chart changes are needed, since the Deployment always wires each field via `secretKeyRef`: + + ```sh + helm upgrade --install logflare ./helm -f my-values.yaml + ``` + +Because each field is wired independently, you can also split fields across multiple `ExternalSecret`/Secret objects (e.g. one per upstream secret-store path) — just set a different `existingSecret` name per field. ## Configurable values @@ -123,7 +218,7 @@ Tools like Sealed Secrets or the External Secrets Operator work out of the box w | `logflare.backend.bigquery.datasetIdAppend` | `GOOGLE_DATASET_ID_APPEND` | bigquery only | | `logflare.backend.bigquery.datasetLocation` | `GOOGLE_DATASET_LOCATION` | bigquery only | | `logflare.backend.postgres.schema` | `POSTGRES_BACKEND_SCHEMA` | postgres only | -| `logflare.secrets.*` / `logflare.existingSecret` | see [Loading secrets](#loading-secrets) | | +| `logflare.secrets.*` | see [Loading secrets](#loading-secrets) | | See `values.yaml` for the full set of generic chart values (image, service, ingress, resources, autoscaling, etc.). diff --git a/helm/configmap_test.yaml b/helm/configmap_test.yaml index 3202c2e418..e52703a6ba 100644 --- a/helm/configmap_test.yaml +++ b/helm/configmap_test.yaml @@ -51,6 +51,17 @@ tests: path: stringData.PHX_LIVE_VIEW_SIGNING_SALT template: secret.yaml + - it: should omit a field's key from the chart's own Secret when existingSecret is set for it + set: + logflare.secrets.publicAccessToken.existingSecret: my-custom-secret + asserts: + - notExists: + path: stringData.LOGFLARE_PUBLIC_ACCESS_TOKEN + template: secret.yaml + - isNotNull: + path: stringData.LOGFLARE_PRIVATE_ACCESS_TOKEN + template: secret.yaml + - it: should omit empty optional configmap keys asserts: - notExists: diff --git a/helm/deployment_test.yaml b/helm/deployment_test.yaml index b10a45d8ec..25d8719064 100644 --- a/helm/deployment_test.yaml +++ b/helm/deployment_test.yaml @@ -11,43 +11,101 @@ tests: value: Deployment template: deployment.yaml - - it: should reference the chart's ConfigMap and Secret via envFrom by default + - it: should reference the chart's ConfigMap via envFrom by default asserts: - equal: path: spec.template.spec.containers[0].envFrom[0].configMapRef.name value: RELEASE-NAME-logflare template: deployment.yaml + - notExists: + path: spec.template.spec.containers[0].envFrom[1] + template: deployment.yaml + + - it: should wire each secret field via secretKeyRef pointing at the chart's own Secret by default + asserts: + - equal: + path: spec.template.spec.containers[0].env[1].name + value: LOGFLARE_PUBLIC_ACCESS_TOKEN + template: deployment.yaml - equal: - path: spec.template.spec.containers[0].envFrom[1].secretRef.name + path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.name value: RELEASE-NAME-logflare template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key + value: LOGFLARE_PUBLIC_ACCESS_TOKEN + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[2].name + value: LOGFLARE_PRIVATE_ACCESS_TOKEN + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[3].name + value: DB_PASSWORD + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[4].name + value: LOGFLARE_DB_ENCRYPTION_KEY + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[5].name + value: PHX_SECRET_KEY_BASE + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[6].name + value: PHX_LIVE_VIEW_SIGNING_SALT + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[7].name + value: GOOGLE_SERVICE_ACCOUNT + template: deployment.yaml - - it: should reference an existingSecret when set, instead of the chart's own Secret + - it: should wire POSTGRES_BACKEND_URL instead of GOOGLE_SERVICE_ACCOUNT when backend.type is postgres set: - logflare.existingSecret: my-custom-secret + logflare.backend.type: postgres asserts: - equal: - path: spec.template.spec.containers[0].envFrom[1].secretRef.name - value: my-custom-secret + path: spec.template.spec.containers[0].env[7].name + value: POSTGRES_BACKEND_URL template: deployment.yaml - - it: should render checksum/config annotation, and checksum/secret only when not using existingSecret + - it: should reference an existingSecret/existingSecretKey for a single field when set, leaving others on the chart's own Secret + set: + logflare.secrets.publicAccessToken.existingSecret: my-custom-secret + logflare.secrets.publicAccessToken.existingSecretKey: my-custom-key asserts: - - isNotNull: - path: spec.template.metadata.annotations["checksum/config"] + - equal: + path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.name + value: my-custom-secret template: deployment.yaml - - isNotNull: - path: spec.template.metadata.annotations["checksum/secret"] + - equal: + path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key + value: my-custom-key + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[2].valueFrom.secretKeyRef.name + value: RELEASE-NAME-logflare template: deployment.yaml - - it: should omit checksum/secret annotation when using an existingSecret + - it: should default existingSecretKey to the chart's own key name when existingSecret is set without a key set: - logflare.existingSecret: my-custom-secret + logflare.secrets.dbPassword.existingSecret: my-custom-secret + asserts: + - equal: + path: spec.template.spec.containers[0].env[3].valueFrom.secretKeyRef.name + value: my-custom-secret + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].env[3].valueFrom.secretKeyRef.key + value: DB_PASSWORD + template: deployment.yaml + + - it: should always render checksum/config and checksum/secret annotations asserts: - isNotNull: path: spec.template.metadata.annotations["checksum/config"] template: deployment.yaml - - notExists: + - isNotNull: path: spec.template.metadata.annotations["checksum/secret"] template: deployment.yaml diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 304c1221bb..7db3d12fd1 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -60,3 +60,18 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Render an env entry sourced via secretKeyRef, pointing at either the chart's +own Secret or an externally-managed one when .field.existingSecret is set. +Expects a dict with: root (the root context $), field (a logflare.secrets.* +entry), envName (the env var name), defaultKey (the key used in the chart's +own Secret). +*/}} +{{- define "logflare.secretEnv" -}} +- name: {{ .envName }} + valueFrom: + secretKeyRef: + name: {{ .field.existingSecret | default (include "logflare.fullname" .root) }} + key: {{ .field.existingSecretKey | default .defaultKey | quote }} +{{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index d6b9b85e6a..cd4bd452cb 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -15,9 +15,7 @@ spec: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - {{- if not .Values.logflare.existingSecret }} checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - {{- end }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} @@ -61,11 +59,20 @@ spec: fieldRef: fieldPath: status.podIP {{- end }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.publicAccessToken "envName" "LOGFLARE_PUBLIC_ACCESS_TOKEN" "defaultKey" "LOGFLARE_PUBLIC_ACCESS_TOKEN") | nindent 12 }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.privateAccessToken "envName" "LOGFLARE_PRIVATE_ACCESS_TOKEN" "defaultKey" "LOGFLARE_PRIVATE_ACCESS_TOKEN") | nindent 12 }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.dbPassword "envName" "DB_PASSWORD" "defaultKey" "DB_PASSWORD") | nindent 12 }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.dbEncryptionKey "envName" "LOGFLARE_DB_ENCRYPTION_KEY" "defaultKey" "LOGFLARE_DB_ENCRYPTION_KEY") | nindent 12 }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.phxSecretKeyBase "envName" "PHX_SECRET_KEY_BASE" "defaultKey" "PHX_SECRET_KEY_BASE") | nindent 12 }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.phxLiveViewSigningSalt "envName" "PHX_LIVE_VIEW_SIGNING_SALT" "defaultKey" "PHX_LIVE_VIEW_SIGNING_SALT") | nindent 12 }} + {{- if eq .Values.logflare.backend.type "postgres" }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.postgresBackendUrl "envName" "POSTGRES_BACKEND_URL" "defaultKey" "POSTGRES_BACKEND_URL") | nindent 12 }} + {{- else if eq .Values.logflare.backend.type "bigquery" }} + {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.googleServiceAccountJson "envName" "GOOGLE_SERVICE_ACCOUNT" "defaultKey" "GOOGLE_SERVICE_ACCOUNT") | nindent 12 }} + {{- end }} envFrom: - configMapRef: name: {{ include "logflare.fullname" . }} - - secretRef: - name: {{ .Values.logflare.existingSecret | default (include "logflare.fullname" .) }} {{- with .Values.livenessProbe }} livenessProbe: {{- toYaml . | nindent 12 }} diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml index c2685b748d..8003a24a4e 100644 --- a/helm/templates/secret.yaml +++ b/helm/templates/secret.yaml @@ -1,4 +1,3 @@ -{{- if not .Values.logflare.existingSecret }} apiVersion: v1 kind: Secret metadata: @@ -7,15 +6,30 @@ metadata: {{- include "logflare.labels" . | nindent 4 }} type: Opaque stringData: - LOGFLARE_PUBLIC_ACCESS_TOKEN: {{ .Values.logflare.secrets.publicAccessToken | quote }} - LOGFLARE_PRIVATE_ACCESS_TOKEN: {{ .Values.logflare.secrets.privateAccessToken | quote }} - DB_PASSWORD: {{ .Values.logflare.secrets.dbPassword | quote }} - LOGFLARE_DB_ENCRYPTION_KEY: {{ .Values.logflare.secrets.dbEncryptionKey | quote }} - PHX_SECRET_KEY_BASE: {{ .Values.logflare.secrets.phxSecretKeyBase | quote }} - PHX_LIVE_VIEW_SIGNING_SALT: {{ .Values.logflare.secrets.phxLiveViewSigningSalt | quote }} + {{- if not .Values.logflare.secrets.publicAccessToken.existingSecret }} + LOGFLARE_PUBLIC_ACCESS_TOKEN: {{ .Values.logflare.secrets.publicAccessToken.value | quote }} + {{- end }} + {{- if not .Values.logflare.secrets.privateAccessToken.existingSecret }} + LOGFLARE_PRIVATE_ACCESS_TOKEN: {{ .Values.logflare.secrets.privateAccessToken.value | quote }} + {{- end }} + {{- if not .Values.logflare.secrets.dbPassword.existingSecret }} + DB_PASSWORD: {{ .Values.logflare.secrets.dbPassword.value | quote }} + {{- end }} + {{- if not .Values.logflare.secrets.dbEncryptionKey.existingSecret }} + LOGFLARE_DB_ENCRYPTION_KEY: {{ .Values.logflare.secrets.dbEncryptionKey.value | quote }} + {{- end }} + {{- if not .Values.logflare.secrets.phxSecretKeyBase.existingSecret }} + PHX_SECRET_KEY_BASE: {{ .Values.logflare.secrets.phxSecretKeyBase.value | quote }} + {{- end }} + {{- if not .Values.logflare.secrets.phxLiveViewSigningSalt.existingSecret }} + PHX_LIVE_VIEW_SIGNING_SALT: {{ .Values.logflare.secrets.phxLiveViewSigningSalt.value | quote }} + {{- end }} {{- if eq .Values.logflare.backend.type "postgres" }} - POSTGRES_BACKEND_URL: {{ .Values.logflare.secrets.postgresBackendUrl | quote }} + {{- if not .Values.logflare.secrets.postgresBackendUrl.existingSecret }} + POSTGRES_BACKEND_URL: {{ .Values.logflare.secrets.postgresBackendUrl.value | quote }} + {{- end }} {{- else if eq .Values.logflare.backend.type "bigquery" }} - GOOGLE_SERVICE_ACCOUNT: {{ .Values.logflare.secrets.googleServiceAccountJson | quote }} + {{- if not .Values.logflare.secrets.googleServiceAccountJson.existingSecret }} + GOOGLE_SERVICE_ACCOUNT: {{ .Values.logflare.secrets.googleServiceAccountJson.value | quote }} + {{- end }} {{- end }} -{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 5def8fe008..3ac8f9cfa6 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -14,8 +14,9 @@ image: tag: "" # Logflare single-tenant application configuration. -# These values are rendered into a ConfigMap (non-sensitive) and a Secret -# (sensitive) which are wired into the Deployment via envFrom. +# Non-sensitive values are rendered into a ConfigMap and wired into the +# Deployment via envFrom. Sensitive values (logflare.secrets.*) are wired in +# individually via secretKeyRef - see the comment above logflare.secrets. logflare: singleTenant: true supabaseMode: false @@ -34,23 +35,50 @@ logflare: urlPort: 443 checkOrigin: false - # Sensitive values. Either set them inline here (rendered into a Secret - # this chart creates), or set existingSecret to the name of a Secret you - # manage yourself containing the same keys (see templates/secret.yaml). - existingSecret: "" + # Sensitive values. For each field, either set `value` inline (rendered + # into a Secret this chart creates), or set `existingSecret`/ + # `existingSecretKey` to reference a key in a Secret you manage yourself + # (e.g. one materialized by External Secrets Operator). When + # `existingSecret` is set, `value` and `existingSecretKey` (defaults to the + # chart's own key name) are used to build a secretKeyRef instead of + # rendering that key into the chart's own Secret. secrets: - publicAccessToken: "" - privateAccessToken: "" - dbPassword: "" - dbEncryptionKey: "" + publicAccessToken: + value: "" + existingSecret: "" + existingSecretKey: "" + privateAccessToken: + value: "" + existingSecret: "" + existingSecretKey: "" + dbPassword: + value: "" + existingSecret: "" + existingSecretKey: "" + dbEncryptionKey: + value: "" + existingSecret: "" + existingSecretKey: "" # Phoenix session/cookie signing. Required for the app to boot. - phxSecretKeyBase: "" - phxLiveViewSigningSalt: "" + phxSecretKeyBase: + value: "" + existingSecret: "" + existingSecretKey: "" + phxLiveViewSigningSalt: + value: "" + existingSecret: "" + existingSecretKey: "" # Only used when backend.type == postgres - postgresBackendUrl: "" + postgresBackendUrl: + value: "" + existingSecret: "" + existingSecretKey: "" # Only used when backend.type == bigquery. Contents of the GCP service # account JSON key file. - googleServiceAccountJson: "" + googleServiceAccountJson: + value: "" + existingSecret: "" + existingSecretKey: "" # Logflare's own metadata Postgres database (distinct from the optional # "postgres" event-storage backend below). From b0aa607c7f8953a6627272c3d261f5e67e459adc Mon Sep 17 00:00:00 2001 From: TzeYiing Date: Thu, 23 Jul 2026 16:21:47 +0800 Subject: [PATCH 05/11] chore: remove secrets.yaml --- helm/README.md | 133 +++++++++------------------------ helm/configmap_test.yaml | 33 -------- helm/deployment_test.yaml | 81 ++++---------------- helm/templates/_helpers.tpl | 15 ---- helm/templates/deployment.yaml | 16 +--- helm/templates/secret.yaml | 35 --------- helm/values.yaml | 61 ++++----------- 7 files changed, 69 insertions(+), 305 deletions(-) delete mode 100644 helm/templates/secret.yaml diff --git a/helm/README.md b/helm/README.md index ad62cf406e..0faf1ce211 100644 --- a/helm/README.md +++ b/helm/README.md @@ -41,94 +41,54 @@ The connection URL (`POSTGRES_BACKEND_URL`) is a secret and is not set here — ## Loading secrets -The chart needs several sensitive values, each configured under its own key in `logflare.secrets.*`: - -| `logflare.secrets.*` key | Env var | Notes | -|---|---|---| -| `publicAccessToken` | `LOGFLARE_PUBLIC_ACCESS_TOKEN` | | -| `privateAccessToken` | `LOGFLARE_PRIVATE_ACCESS_TOKEN` | | -| `dbPassword` | `DB_PASSWORD` | | -| `dbEncryptionKey` | `LOGFLARE_DB_ENCRYPTION_KEY` | | -| `phxSecretKeyBase` | `PHX_SECRET_KEY_BASE` | e.g. `openssl rand -base64 48` | -| `phxLiveViewSigningSalt` | `PHX_LIVE_VIEW_SIGNING_SALT` | e.g. `openssl rand -base64 8` | -| `postgresBackendUrl` | `POSTGRES_BACKEND_URL` | postgres backend only | -| `googleServiceAccountJson` | `GOOGLE_SERVICE_ACCOUNT` | bigquery backend only, the raw service-account JSON key content | - -Each field is its own object with three sub-keys: - -```yaml -logflare: - secrets: - : - value: "" # inline value, rendered into the chart's own Secret - existingSecret: "" # OR: name of a Secret that already exists in the namespace - existingSecretKey: "" # key within that Secret (defaults to the chart's own key name above) -``` - -For each field independently: if `existingSecret` is empty, the chart renders that key into the Secret it creates (named after the release) using `value`. If `existingSecret` is set, the chart instead wires that env var via `secretKeyRef` straight to `existingSecret`/`existingSecretKey`, and **does not** render that key into its own Secret. You can mix both approaches — e.g. keep `dbPassword` inline while sourcing `googleServiceAccountJson` from an externally managed Secret. - -### 1. Inline via values (quick/dev use) +**This chart never creates or owns a `Secret`.** You provision the Secret(s) yourself (e.g. via `kubectl create secret`, or via a tool like External Secrets Operator) with keys already named the way Logflare expects, and list their names in `logflare.secretRefs`. Every key in each listed Secret is injected in bulk via `envFrom`, the same way the chart's own ConfigMap is: ```yaml logflare: - secrets: - publicAccessToken: - value: "..." - privateAccessToken: - value: "..." - dbPassword: - value: "..." - dbEncryptionKey: - value: "..." - phxSecretKeyBase: - value: "..." # e.g. `openssl rand -base64 48` - phxLiveViewSigningSalt: - value: "..." # e.g. `openssl rand -base64 8` - postgresBackendUrl: - value: "postgresql://user:pass@host:5432/db" # postgres backend - # googleServiceAccountJson: - # value: '{"type": "service_account", ...}' # bigquery backend + secretRefs: + - logflare-secrets ``` -Keep this in a values file that is *not* committed to source control (e.g. `secrets-values.yaml`), and pass it alongside your other values: - -```sh -helm install logflare ./helm -f my-values.yaml -f secrets-values.yaml -``` +Unlike a per-variable mapping, this means **the Secret's key names must exactly match the env vars Logflare reads** (see the table below) — there's no renaming step. If you need to source individual vars from differently-named keys, or split them across Secrets with arbitrary key names, create multiple Secrets with the right key names upstream (e.g. via multiple `ExternalSecret` objects) and list all of them in `secretRefs`. -This is the simplest option, but the values end up stored in the Helm release's state (in-cluster), which is not ideal for production. +Most deployments need at least these env vars, so your Secret(s) should contain these keys: -### 2. Referencing an existing Secret (recommended for production) +| Env var | Notes | +|---|---| +| `LOGFLARE_PUBLIC_ACCESS_TOKEN` | | +| `LOGFLARE_PRIVATE_ACCESS_TOKEN` | | +| `DB_PASSWORD` | | +| `LOGFLARE_DB_ENCRYPTION_KEY` | | +| `PHX_SECRET_KEY_BASE` | e.g. `openssl rand -base64 48` | +| `PHX_LIVE_VIEW_SIGNING_SALT` | e.g. `openssl rand -base64 8` | +| `POSTGRES_BACKEND_URL` | postgres backend only | +| `GOOGLE_SERVICE_ACCOUNT` | bigquery backend only, the raw service-account JSON key content | -Create the Secret yourself, outside of Helm, then point the relevant field(s) at it via `existingSecret`/`existingSecretKey`. Unlike the previous whole-chart `existingSecret` toggle, this is per field — the Secret's key names don't need to match the chart's own, and different fields can point at entirely different Secrets: +### Creating the Secret yourself ```sh kubectl create secret generic logflare-secrets \ - --from-literal=public-access-token=... \ - --from-literal=private-access-token=... \ - --from-literal=db-password=... + --from-literal=LOGFLARE_PUBLIC_ACCESS_TOKEN=... \ + --from-literal=LOGFLARE_PRIVATE_ACCESS_TOKEN=... \ + --from-literal=DB_PASSWORD=... \ + --from-literal=LOGFLARE_DB_ENCRYPTION_KEY=... \ + --from-literal=PHX_SECRET_KEY_BASE=... \ + --from-literal=PHX_LIVE_VIEW_SIGNING_SALT=... \ + --from-literal=POSTGRES_BACKEND_URL=postgresql://user:pass@host:5432/db + # or: --from-file=GOOGLE_SERVICE_ACCOUNT=./gcloud.json (bigquery backend) ``` ```yaml logflare: - secrets: - publicAccessToken: - existingSecret: "logflare-secrets" - existingSecretKey: "public-access-token" - privateAccessToken: - existingSecret: "logflare-secrets" - existingSecretKey: "private-access-token" - dbPassword: - existingSecret: "logflare-secrets" - existingSecretKey: "db-password" - # remaining fields fall back to `value` / the chart's own Secret + secretRefs: + - logflare-secrets ``` -### 3. External Secrets Operator (ESO) +### External Secrets Operator (ESO) -[External Secrets Operator](https://external-secrets.io/) is the recommended way to run this in production: it syncs values from a real secret manager (AWS Secrets Manager, GCP Secret Manager, Vault, etc.) into a plain Kubernetes `Secret` that this chart then references via `existingSecret`. +[External Secrets Operator](https://external-secrets.io/) is the recommended way to run this in production: it syncs values from a real secret manager (AWS Secrets Manager, GCP Secret Manager, Vault, etc.) into a plain Kubernetes `Secret`, which this chart then references via `logflare.secretRefs`. -1. **Provision an `ExternalSecret`** (outside this chart, e.g. in your cluster-config repo) that materializes the Secret this chart will consume: +1. **Provision an `ExternalSecret`** (outside this chart, e.g. in your cluster-config repo) that materializes the Secret this chart will consume, with keys already named to match what Logflare expects: ```yaml apiVersion: external-secrets.io/v1 @@ -151,44 +111,23 @@ logflare: key: prod/logflare/secrets ``` - This produces a Secret named `logflare-secrets` in the namespace, with whatever keys exist at `prod/logflare/secrets` in your secret store (e.g. `public_access_token`, `private_access_token`, `db_password`, ...). ESO owns creation/rotation of this Secret entirely; the chart never sees the underlying values. + If the secrets in your secret store at `prod/logflare/secrets` don't already use Logflare's expected key names (e.g. `public_access_token` instead of `LOGFLARE_PUBLIC_ACCESS_TOKEN`), use `data:` entries with explicit `secretKey`/`remoteRef` pairs (or a templated `target.template`) instead of `dataFrom.extract`, so the resulting Secret's keys match exactly — ESO owns creation/rotation of this Secret entirely, and the chart injects whatever keys land in it verbatim. -2. **Point the chart's fields at it**, matching each field to the key your secret store uses: +2. **List the Secret's name in `logflare.secretRefs`:** ```yaml logflare: - secrets: - publicAccessToken: - existingSecret: "logflare-secrets" - existingSecretKey: "public_access_token" - privateAccessToken: - existingSecret: "logflare-secrets" - existingSecretKey: "private_access_token" - dbPassword: - existingSecret: "logflare-secrets" - existingSecretKey: "db_password" - dbEncryptionKey: - existingSecret: "logflare-secrets" - existingSecretKey: "db_encryption_key" - phxSecretKeyBase: - existingSecret: "logflare-secrets" - existingSecretKey: "phx_secret_key_base" - phxLiveViewSigningSalt: - existingSecret: "logflare-secrets" - existingSecretKey: "phx_live_view_signing_salt" - # bigquery backend: - googleServiceAccountJson: - existingSecret: "logflare-secrets" - existingSecretKey: "google_service_account_json" + secretRefs: + - logflare-secrets ``` -3. **Install/upgrade as usual** — no chart changes are needed, since the Deployment always wires each field via `secretKeyRef`: +3. **Install/upgrade as usual** — no chart changes are needed, since the Deployment always wires every listed Secret in via `envFrom.secretRef`: ```sh helm upgrade --install logflare ./helm -f my-values.yaml ``` -Because each field is wired independently, you can also split fields across multiple `ExternalSecret`/Secret objects (e.g. one per upstream secret-store path) — just set a different `existingSecret` name per field. +You can list multiple Secret names in `secretRefs` (e.g. one per `ExternalSecret`/upstream secret-store path) — all of their keys are injected together. ## Configurable values @@ -218,7 +157,7 @@ Because each field is wired independently, you can also split fields across mult | `logflare.backend.bigquery.datasetIdAppend` | `GOOGLE_DATASET_ID_APPEND` | bigquery only | | `logflare.backend.bigquery.datasetLocation` | `GOOGLE_DATASET_LOCATION` | bigquery only | | `logflare.backend.postgres.schema` | `POSTGRES_BACKEND_SCHEMA` | postgres only | -| `logflare.secrets.*` | see [Loading secrets](#loading-secrets) | | +| `logflare.secretRefs` | see [Loading secrets](#loading-secrets) | | See `values.yaml` for the full set of generic chart values (image, service, ingress, resources, autoscaling, etc.). diff --git a/helm/configmap_test.yaml b/helm/configmap_test.yaml index e52703a6ba..31ec417310 100644 --- a/helm/configmap_test.yaml +++ b/helm/configmap_test.yaml @@ -1,7 +1,6 @@ suite: test backend wiring templates: - configmap.yaml - - secret.yaml tests: - it: should render bigquery vars and omit postgres vars by default asserts: @@ -15,12 +14,6 @@ tests: - isNull: path: data.POSTGRES_BACKEND_SCHEMA template: configmap.yaml - - isNotNull: - path: stringData.GOOGLE_SERVICE_ACCOUNT - template: secret.yaml - - isNull: - path: stringData.POSTGRES_BACKEND_URL - template: secret.yaml - it: should render postgres vars and omit bigquery vars when backend.type is postgres set: @@ -35,32 +28,6 @@ tests: - isNull: path: data.GOOGLE_PROJECT_NUMBER template: configmap.yaml - - isNotNull: - path: stringData.POSTGRES_BACKEND_URL - template: secret.yaml - - isNull: - path: stringData.GOOGLE_SERVICE_ACCOUNT - template: secret.yaml - - - it: should always render the phoenix session secrets - asserts: - - isNotNull: - path: stringData.PHX_SECRET_KEY_BASE - template: secret.yaml - - isNotNull: - path: stringData.PHX_LIVE_VIEW_SIGNING_SALT - template: secret.yaml - - - it: should omit a field's key from the chart's own Secret when existingSecret is set for it - set: - logflare.secrets.publicAccessToken.existingSecret: my-custom-secret - asserts: - - notExists: - path: stringData.LOGFLARE_PUBLIC_ACCESS_TOKEN - template: secret.yaml - - isNotNull: - path: stringData.LOGFLARE_PRIVATE_ACCESS_TOKEN - template: secret.yaml - it: should omit empty optional configmap keys asserts: diff --git a/helm/deployment_test.yaml b/helm/deployment_test.yaml index 25d8719064..97f8cf7704 100644 --- a/helm/deployment_test.yaml +++ b/helm/deployment_test.yaml @@ -2,7 +2,6 @@ suite: test deployment kind templates: - deployment.yaml - configmap.yaml - - secret.yaml tests: - it: should render the correct deployment Kind asserts: @@ -21,91 +20,41 @@ tests: path: spec.template.spec.containers[0].envFrom[1] template: deployment.yaml - - it: should wire each secret field via secretKeyRef pointing at the chart's own Secret by default + - it: should reference no Secrets via envFrom by default asserts: - equal: - path: spec.template.spec.containers[0].env[1].name - value: LOGFLARE_PUBLIC_ACCESS_TOKEN - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.name + path: spec.template.spec.containers[0].envFrom[0].configMapRef.name value: RELEASE-NAME-logflare template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key - value: LOGFLARE_PUBLIC_ACCESS_TOKEN - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[2].name - value: LOGFLARE_PRIVATE_ACCESS_TOKEN - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[3].name - value: DB_PASSWORD - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[4].name - value: LOGFLARE_DB_ENCRYPTION_KEY - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[5].name - value: PHX_SECRET_KEY_BASE - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[6].name - value: PHX_LIVE_VIEW_SIGNING_SALT - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[7].name - value: GOOGLE_SERVICE_ACCOUNT - template: deployment.yaml - - - it: should wire POSTGRES_BACKEND_URL instead of GOOGLE_SERVICE_ACCOUNT when backend.type is postgres - set: - logflare.backend.type: postgres - asserts: - - equal: - path: spec.template.spec.containers[0].env[7].name - value: POSTGRES_BACKEND_URL + - notExists: + path: spec.template.spec.containers[0].envFrom[1] template: deployment.yaml - - it: should reference an existingSecret/existingSecretKey for a single field when set, leaving others on the chart's own Secret + - it: should reference each Secret in logflare.secretRefs via envFrom set: - logflare.secrets.publicAccessToken.existingSecret: my-custom-secret - logflare.secrets.publicAccessToken.existingSecretKey: my-custom-key + logflare.secretRefs: + - logflare-secrets + - logflare-db-secret asserts: - equal: - path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.name - value: my-custom-secret - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.key - value: my-custom-key - template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].env[2].valueFrom.secretKeyRef.name + path: spec.template.spec.containers[0].envFrom[0].configMapRef.name value: RELEASE-NAME-logflare template: deployment.yaml - - - it: should default existingSecretKey to the chart's own key name when existingSecret is set without a key - set: - logflare.secrets.dbPassword.existingSecret: my-custom-secret - asserts: - equal: - path: spec.template.spec.containers[0].env[3].valueFrom.secretKeyRef.name - value: my-custom-secret + path: spec.template.spec.containers[0].envFrom[1].secretRef.name + value: logflare-secrets template: deployment.yaml - equal: - path: spec.template.spec.containers[0].env[3].valueFrom.secretKeyRef.key - value: DB_PASSWORD + path: spec.template.spec.containers[0].envFrom[2].secretRef.name + value: logflare-db-secret template: deployment.yaml - - it: should always render checksum/config and checksum/secret annotations + - it: should always render checksum/config annotation asserts: - isNotNull: path: spec.template.metadata.annotations["checksum/config"] template: deployment.yaml - - isNotNull: + - notExists: path: spec.template.metadata.annotations["checksum/secret"] template: deployment.yaml diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 7db3d12fd1..304c1221bb 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -60,18 +60,3 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} - -{{/* -Render an env entry sourced via secretKeyRef, pointing at either the chart's -own Secret or an externally-managed one when .field.existingSecret is set. -Expects a dict with: root (the root context $), field (a logflare.secrets.* -entry), envName (the env var name), defaultKey (the key used in the chart's -own Secret). -*/}} -{{- define "logflare.secretEnv" -}} -- name: {{ .envName }} - valueFrom: - secretKeyRef: - name: {{ .field.existingSecret | default (include "logflare.fullname" .root) }} - key: {{ .field.existingSecretKey | default .defaultKey | quote }} -{{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index cd4bd452cb..d00afdd1f0 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -15,7 +15,6 @@ spec: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} @@ -59,20 +58,13 @@ spec: fieldRef: fieldPath: status.podIP {{- end }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.publicAccessToken "envName" "LOGFLARE_PUBLIC_ACCESS_TOKEN" "defaultKey" "LOGFLARE_PUBLIC_ACCESS_TOKEN") | nindent 12 }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.privateAccessToken "envName" "LOGFLARE_PRIVATE_ACCESS_TOKEN" "defaultKey" "LOGFLARE_PRIVATE_ACCESS_TOKEN") | nindent 12 }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.dbPassword "envName" "DB_PASSWORD" "defaultKey" "DB_PASSWORD") | nindent 12 }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.dbEncryptionKey "envName" "LOGFLARE_DB_ENCRYPTION_KEY" "defaultKey" "LOGFLARE_DB_ENCRYPTION_KEY") | nindent 12 }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.phxSecretKeyBase "envName" "PHX_SECRET_KEY_BASE" "defaultKey" "PHX_SECRET_KEY_BASE") | nindent 12 }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.phxLiveViewSigningSalt "envName" "PHX_LIVE_VIEW_SIGNING_SALT" "defaultKey" "PHX_LIVE_VIEW_SIGNING_SALT") | nindent 12 }} - {{- if eq .Values.logflare.backend.type "postgres" }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.postgresBackendUrl "envName" "POSTGRES_BACKEND_URL" "defaultKey" "POSTGRES_BACKEND_URL") | nindent 12 }} - {{- else if eq .Values.logflare.backend.type "bigquery" }} - {{- include "logflare.secretEnv" (dict "root" $ "field" .Values.logflare.secrets.googleServiceAccountJson "envName" "GOOGLE_SERVICE_ACCOUNT" "defaultKey" "GOOGLE_SERVICE_ACCOUNT") | nindent 12 }} - {{- end }} envFrom: - configMapRef: name: {{ include "logflare.fullname" . }} + {{- range .Values.logflare.secretRefs }} + - secretRef: + name: {{ . }} + {{- end }} {{- with .Values.livenessProbe }} livenessProbe: {{- toYaml . | nindent 12 }} diff --git a/helm/templates/secret.yaml b/helm/templates/secret.yaml deleted file mode 100644 index 8003a24a4e..0000000000 --- a/helm/templates/secret.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "logflare.fullname" . }} - labels: - {{- include "logflare.labels" . | nindent 4 }} -type: Opaque -stringData: - {{- if not .Values.logflare.secrets.publicAccessToken.existingSecret }} - LOGFLARE_PUBLIC_ACCESS_TOKEN: {{ .Values.logflare.secrets.publicAccessToken.value | quote }} - {{- end }} - {{- if not .Values.logflare.secrets.privateAccessToken.existingSecret }} - LOGFLARE_PRIVATE_ACCESS_TOKEN: {{ .Values.logflare.secrets.privateAccessToken.value | quote }} - {{- end }} - {{- if not .Values.logflare.secrets.dbPassword.existingSecret }} - DB_PASSWORD: {{ .Values.logflare.secrets.dbPassword.value | quote }} - {{- end }} - {{- if not .Values.logflare.secrets.dbEncryptionKey.existingSecret }} - LOGFLARE_DB_ENCRYPTION_KEY: {{ .Values.logflare.secrets.dbEncryptionKey.value | quote }} - {{- end }} - {{- if not .Values.logflare.secrets.phxSecretKeyBase.existingSecret }} - PHX_SECRET_KEY_BASE: {{ .Values.logflare.secrets.phxSecretKeyBase.value | quote }} - {{- end }} - {{- if not .Values.logflare.secrets.phxLiveViewSigningSalt.existingSecret }} - PHX_LIVE_VIEW_SIGNING_SALT: {{ .Values.logflare.secrets.phxLiveViewSigningSalt.value | quote }} - {{- end }} - {{- if eq .Values.logflare.backend.type "postgres" }} - {{- if not .Values.logflare.secrets.postgresBackendUrl.existingSecret }} - POSTGRES_BACKEND_URL: {{ .Values.logflare.secrets.postgresBackendUrl.value | quote }} - {{- end }} - {{- else if eq .Values.logflare.backend.type "bigquery" }} - {{- if not .Values.logflare.secrets.googleServiceAccountJson.existingSecret }} - GOOGLE_SERVICE_ACCOUNT: {{ .Values.logflare.secrets.googleServiceAccountJson.value | quote }} - {{- end }} - {{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 3ac8f9cfa6..4810f6074d 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -15,8 +15,8 @@ image: # Logflare single-tenant application configuration. # Non-sensitive values are rendered into a ConfigMap and wired into the -# Deployment via envFrom. Sensitive values (logflare.secrets.*) are wired in -# individually via secretKeyRef - see the comment above logflare.secrets. +# Deployment via envFrom. Sensitive values are loaded the same way, in bulk, +# from Secret(s) you manage yourself - see the comment above logflare.secretRefs. logflare: singleTenant: true supabaseMode: false @@ -35,50 +35,17 @@ logflare: urlPort: 443 checkOrigin: false - # Sensitive values. For each field, either set `value` inline (rendered - # into a Secret this chart creates), or set `existingSecret`/ - # `existingSecretKey` to reference a key in a Secret you manage yourself - # (e.g. one materialized by External Secrets Operator). When - # `existingSecret` is set, `value` and `existingSecretKey` (defaults to the - # chart's own key name) are used to build a secretKeyRef instead of - # rendering that key into the chart's own Secret. - secrets: - publicAccessToken: - value: "" - existingSecret: "" - existingSecretKey: "" - privateAccessToken: - value: "" - existingSecret: "" - existingSecretKey: "" - dbPassword: - value: "" - existingSecret: "" - existingSecretKey: "" - dbEncryptionKey: - value: "" - existingSecret: "" - existingSecretKey: "" - # Phoenix session/cookie signing. Required for the app to boot. - phxSecretKeyBase: - value: "" - existingSecret: "" - existingSecretKey: "" - phxLiveViewSigningSalt: - value: "" - existingSecret: "" - existingSecretKey: "" - # Only used when backend.type == postgres - postgresBackendUrl: - value: "" - existingSecret: "" - existingSecretKey: "" - # Only used when backend.type == bigquery. Contents of the GCP service - # account JSON key file. - googleServiceAccountJson: - value: "" - existingSecret: "" - existingSecretKey: "" + # This chart never creates or owns a Secret. List the name(s) of Secret(s) + # you manage yourself (e.g. materialized by External Secrets Operator, or + # via `kubectl create secret`) here - every key in each Secret is injected + # in bulk as an env var, the same way the chart's ConfigMap is. At minimum, + # most deployments need a Secret containing: LOGFLARE_PUBLIC_ACCESS_TOKEN, + # LOGFLARE_PRIVATE_ACCESS_TOKEN, DB_PASSWORD, LOGFLARE_DB_ENCRYPTION_KEY, + # PHX_SECRET_KEY_BASE, PHX_LIVE_VIEW_SIGNING_SALT, and one of + # POSTGRES_BACKEND_URL (postgres backend) or GOOGLE_SERVICE_ACCOUNT + # (bigquery backend). See README.md for a full walkthrough. + secretRefs: [] + # - logflare-secrets # Logflare's own metadata Postgres database (distinct from the optional # "postgres" event-storage backend below). @@ -100,7 +67,7 @@ logflare: datasetIdAppend: "_prod" datasetLocation: "US" postgres: - schema: "public" # POSTGRES_BACKEND_SCHEMA; url comes from logflare.secrets.postgresBackendUrl + schema: "public" # POSTGRES_BACKEND_SCHEMA; url (POSTGRES_BACKEND_URL) comes from a Secret in logflare.secretRefs # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ imagePullSecrets: [] From 7a8a569d442c61c08a3b13885ab9b5be73b9167e Mon Sep 17 00:00:00 2001 From: David Whittington Date: Fri, 24 Jul 2026 10:31:17 -0500 Subject: [PATCH 06/11] feat(helm): wire in secret file mounts, reloader, and extra config (#3728) Builds on the initial chart to support the full secret/config surface a production deployment needs: - certFilesSecret / certFilesMountPath: mount a Secret of cert/key files (DB SSL + gRPC TLS) as files, and point the DB_SSL_*_PATH / LOGFLARE_TLS_*_PATH env vars at the mount. Logflare reads these from disk, so envFrom can't carry them. (Requires the configurable-path support in logflare runtime.exs.) - reloader: optional Stakater Reloader annotation so the Deployment rolls when its ConfigMap or referenced Secrets change (the chart can't checksum Secrets it doesn't own). - extraConfig: free-form passthrough of non-secret env vars into the ConfigMap, for the long tail of configuration not modeled as first-class values. Adds helm-unittest coverage for each and documents them in the README. All new behavior is default-off, so existing rendering is unchanged. --- helm/README.md | 19 ++++++++++++ helm/configmap_test.yaml | 15 +++++++++ helm/deployment_test.yaml | 56 ++++++++++++++++++++++++++++++++++ helm/templates/configmap.yaml | 3 ++ helm/templates/deployment.yaml | 35 +++++++++++++++++++-- helm/values.yaml | 19 ++++++++++++ 6 files changed, 145 insertions(+), 2 deletions(-) diff --git a/helm/README.md b/helm/README.md index 0faf1ce211..c71091825f 100644 --- a/helm/README.md +++ b/helm/README.md @@ -158,9 +158,28 @@ You can list multiple Secret names in `secretRefs` (e.g. one per `ExternalSecret | `logflare.backend.bigquery.datasetLocation` | `GOOGLE_DATASET_LOCATION` | bigquery only | | `logflare.backend.postgres.schema` | `POSTGRES_BACKEND_SCHEMA` | postgres only | | `logflare.secretRefs` | see [Loading secrets](#loading-secrets) | | +| `logflare.certFilesSecret` | — | Name of a Secret whose keys are cert filenames; mounted as files. See [Certificate files](#certificate-files) | +| `logflare.certFilesMountPath` | `DB_SSL_*_PATH`, `LOGFLARE_TLS_*_PATH` | Mount path for `certFilesSecret`; the chart points the cert path env vars here | +| `logflare.reloader` | — | When `true`, adds the Stakater Reloader annotation so the Deployment rolls on ConfigMap/Secret changes | +| `logflare.extraConfig` | (any) | Map of non-secret env vars rendered verbatim into the ConfigMap | See `values.yaml` for the full set of generic chart values (image, service, ingress, resources, autoscaling, etc.). +### Certificate files + +Unlike the env-var secrets loaded via `envFrom`, Logflare reads its TLS/mTLS +material from files on disk: the internal database SSL certs (when `DB_SSL` is +enabled) and the gRPC TLS cert/key (when `LOGFLARE_ENABLE_GRPC_SSL` is enabled). + +Provide these via a separate Secret whose keys are the exact filenames — +`db-server-ca.pem`, `db-client-cert.pem`, `db-client-key.pem`, `cert.pem`, +`cert.key` — and set `logflare.certFilesSecret` to its name. The chart mounts it +at `logflare.certFilesMountPath` and sets `DB_SSL_CA_CERT_PATH`, +`DB_SSL_CLIENT_CERT_PATH`, `DB_SSL_CLIENT_KEY_PATH`, `LOGFLARE_TLS_CERT_PATH`, +and `LOGFLARE_TLS_KEY_PATH` to point at the mounted files. (The BigQuery service +account key is handled as an env-var secret via `GOOGLE_APPLICATION_CREDENTIALS_JSON`, +not a mounted file.) + ## Verifying a deployment Render the manifests locally before installing: diff --git a/helm/configmap_test.yaml b/helm/configmap_test.yaml index 31ec417310..f47dd569e9 100644 --- a/helm/configmap_test.yaml +++ b/helm/configmap_test.yaml @@ -51,3 +51,18 @@ tests: path: data.PHX_URL_HOST value: "example.com" template: configmap.yaml + + - it: should render extraConfig entries verbatim into the ConfigMap + set: + logflare.extraConfig: + LOGFLARE_LOG_LEVEL: info + SPOOL_MODE: enabled + asserts: + - equal: + path: data.LOGFLARE_LOG_LEVEL + value: "info" + template: configmap.yaml + - equal: + path: data.SPOOL_MODE + value: "enabled" + template: configmap.yaml diff --git a/helm/deployment_test.yaml b/helm/deployment_test.yaml index 97f8cf7704..60fcf8c0b5 100644 --- a/helm/deployment_test.yaml +++ b/helm/deployment_test.yaml @@ -92,3 +92,59 @@ tests: path: spec.template.spec.containers[0].ports[1].containerPort value: 50051 template: deployment.yaml + + - it: should not mount cert files or set cert paths by default + asserts: + - notExists: + path: spec.template.spec.volumes + template: deployment.yaml + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DB_SSL_CA_CERT_PATH + value: /etc/logflare/certs/db-server-ca.pem + template: deployment.yaml + + - it: should mount the cert files Secret and point cert paths at it when set + set: + logflare.certFilesSecret: logflare-cert-files + asserts: + - equal: + path: spec.template.spec.volumes[0].name + value: cert-files + template: deployment.yaml + - equal: + path: spec.template.spec.volumes[0].secret.secretName + value: logflare-cert-files + template: deployment.yaml + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /etc/logflare/certs + template: deployment.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: DB_SSL_CA_CERT_PATH + value: /etc/logflare/certs/db-server-ca.pem + template: deployment.yaml + - contains: + path: spec.template.spec.containers[0].env + content: + name: LOGFLARE_TLS_CERT_PATH + value: /etc/logflare/certs/cert.pem + template: deployment.yaml + + - it: should add the Reloader annotation only when enabled + asserts: + - notExists: + path: metadata.annotations["reloader.stakater.com/auto"] + template: deployment.yaml + + - it: should add the Reloader annotation when reloader is true + set: + logflare.reloader: true + asserts: + - equal: + path: metadata.annotations["reloader.stakater.com/auto"] + value: "true" + template: deployment.yaml diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 89c39dfdd1..356c33cb47 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -39,3 +39,6 @@ data: {{- else }} {{- fail (printf "logflare.backend.type must be 'bigquery' or 'postgres', got %q" .Values.logflare.backend.type) }} {{- end }} + {{- range $key, $value := .Values.logflare.extraConfig }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index d00afdd1f0..475cc065ae 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -2,6 +2,10 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "logflare.fullname" . }} + {{- if .Values.logflare.reloader }} + annotations: + reloader.stakater.com/auto: "true" + {{- end }} labels: {{- include "logflare.labels" . | nindent 4 }} spec: @@ -58,6 +62,19 @@ spec: fieldRef: fieldPath: status.podIP {{- end }} + {{- if .Values.logflare.certFilesSecret }} + {{- $mount := .Values.logflare.certFilesMountPath }} + - name: DB_SSL_CA_CERT_PATH + value: {{ printf "%s/db-server-ca.pem" $mount | quote }} + - name: DB_SSL_CLIENT_CERT_PATH + value: {{ printf "%s/db-client-cert.pem" $mount | quote }} + - name: DB_SSL_CLIENT_KEY_PATH + value: {{ printf "%s/db-client-key.pem" $mount | quote }} + - name: LOGFLARE_TLS_CERT_PATH + value: {{ printf "%s/cert.pem" $mount | quote }} + - name: LOGFLARE_TLS_KEY_PATH + value: {{ printf "%s/cert.key" $mount | quote }} + {{- end }} envFrom: - configMapRef: name: {{ include "logflare.fullname" . }} @@ -81,13 +98,27 @@ spec: resources: {{- toYaml . | nindent 12 }} {{- end }} - {{- with .Values.volumeMounts }} + {{- if or .Values.logflare.certFilesSecret .Values.volumeMounts }} volumeMounts: + {{- if .Values.logflare.certFilesSecret }} + - name: cert-files + mountPath: {{ .Values.logflare.certFilesMountPath }} + readOnly: true + {{- end }} + {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} + {{- end }} {{- end }} - {{- with .Values.volumes }} + {{- if or .Values.logflare.certFilesSecret .Values.volumes }} volumes: + {{- if .Values.logflare.certFilesSecret }} + - name: cert-files + secret: + secretName: {{ .Values.logflare.certFilesSecret }} + {{- end }} + {{- with .Values.volumes }} {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: diff --git a/helm/values.yaml b/helm/values.yaml index 4810f6074d..47b6885212 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -47,6 +47,25 @@ logflare: secretRefs: [] # - logflare-secrets + # Cert/key files (internal DB SSL and gRPC TLS) come from a mounted Secret + # rather than envFrom, because Logflare reads them from disk. Set to the name + # of a Secret you manage yourself whose keys are the cert filenames + # (db-server-ca.pem, db-client-cert.pem, db-client-key.pem, cert.pem, + # cert.key); empty disables the mount. The chart points the matching + # DB_SSL_*_PATH / LOGFLARE_TLS_*_PATH env vars at the mount for you. + certFilesSecret: "" + certFilesMountPath: /etc/logflare/certs + + # Roll the Deployment automatically when its ConfigMap or referenced Secrets + # change. Requires the Stakater Reloader controller to be installed. + reloader: false + + # Free-form non-secret environment variables rendered verbatim into the + # ConfigMap. Use this for configuration not modeled as first-class values + # above (e.g. LOGFLARE_LOG_LEVEL, LIBCLUSTER_TOPOLOGY, SPOOL_*, GOOGLE_*_SA). + extraConfig: {} + # LOGFLARE_LOG_LEVEL: info + # Logflare's own metadata Postgres database (distinct from the optional # "postgres" event-storage backend below). db: From c7ae510e3ad94e1ab111ffba357f67640e8179d8 Mon Sep 17 00:00:00 2001 From: David Whittington Date: Fri, 24 Jul 2026 11:54:56 -0500 Subject: [PATCH 07/11] feat(helm): support custom vm.args via RELEASE_VM_ARGS (#3735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in `vmArgs` value rendered into a ConfigMap, mounted at /etc/logflare/vm.args, and wired via RELEASE_VM_ARGS. Defaults to a set including `+Q 1048576`, which caps the BEAM's preallocated port table — needed on Kubernetes, where containerd runs pods with nofile=infinity and the VM would otherwise size the table from that (~2GB RSS at boot). Mounted with subPath so it coexists with the cert-files mount under /etc/logflare. A checksum/vm-args pod annotation rolls the Deployment when the args change. Set vmArgs to "" to fall back to the image's baked-in vm.args. Adds helm-unittest coverage. --- helm/templates/deployment.yaml | 22 ++++++++++- helm/templates/vm-args-configmap.yaml | 11 ++++++ helm/values.yaml | 17 +++++++++ helm/vmargs_test.yaml | 54 +++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 helm/templates/vm-args-configmap.yaml create mode 100644 helm/vmargs_test.yaml diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 475cc065ae..340f02e0f6 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -19,6 +19,9 @@ spec: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if .Values.vmArgs }} + checksum/vm-args: {{ include (print $.Template.BasePath "/vm-args-configmap.yaml") . | sha256sum }} + {{- end }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} @@ -75,6 +78,10 @@ spec: - name: LOGFLARE_TLS_KEY_PATH value: {{ printf "%s/cert.key" $mount | quote }} {{- end }} + {{- if .Values.vmArgs }} + - name: RELEASE_VM_ARGS + value: /etc/logflare/vm.args + {{- end }} envFrom: - configMapRef: name: {{ include "logflare.fullname" . }} @@ -98,8 +105,14 @@ spec: resources: {{- toYaml . | nindent 12 }} {{- end }} - {{- if or .Values.logflare.certFilesSecret .Values.volumeMounts }} + {{- if or .Values.vmArgs .Values.logflare.certFilesSecret .Values.volumeMounts }} volumeMounts: + {{- if .Values.vmArgs }} + - name: vm-args + mountPath: /etc/logflare/vm.args + subPath: vm.args + readOnly: true + {{- end }} {{- if .Values.logflare.certFilesSecret }} - name: cert-files mountPath: {{ .Values.logflare.certFilesMountPath }} @@ -109,8 +122,13 @@ spec: {{- toYaml . | nindent 12 }} {{- end }} {{- end }} - {{- if or .Values.logflare.certFilesSecret .Values.volumes }} + {{- if or .Values.vmArgs .Values.logflare.certFilesSecret .Values.volumes }} volumes: + {{- if .Values.vmArgs }} + - name: vm-args + configMap: + name: {{ include "logflare.fullname" . }}-vm-args + {{- end }} {{- if .Values.logflare.certFilesSecret }} - name: cert-files secret: diff --git a/helm/templates/vm-args-configmap.yaml b/helm/templates/vm-args-configmap.yaml new file mode 100644 index 0000000000..2047b30ec3 --- /dev/null +++ b/helm/templates/vm-args-configmap.yaml @@ -0,0 +1,11 @@ +{{- if .Values.vmArgs }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "logflare.fullname" . }}-vm-args + labels: + {{- include "logflare.labels" . | nindent 4 }} +data: + vm.args: | + {{- .Values.vmArgs | nindent 4 }} +{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 47b6885212..ef54a91c65 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -88,6 +88,23 @@ logflare: postgres: schema: "public" # POSTGRES_BACKEND_SCHEMA; url (POSTGRES_BACKEND_URL) comes from a Secret in logflare.secretRefs +# Erlang VM flags. When set, rendered into a ConfigMap, mounted at +# /etc/logflare/vm.args, and pointed at via RELEASE_VM_ARGS (overriding the +# image's baked-in vm.args). Set to "" to use the image default instead. +# +# `+Q` caps the BEAM's preallocated port table and is effectively REQUIRED on +# Kubernetes: containerd commonly runs pods with nofile=infinity, and without +# +Q the VM sizes the table from that limit — ~2GB RSS at boot. Keep +Q set. +vmArgs: | + +Q 1048576 + +sbwt none + +sbwtdcpu none + +sbwtdio none + +swt very_low + +P 8000000 + -kernel prevent_overlapping_partitions false + -kernel net_ticktime 45 + # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ imagePullSecrets: [] # This is to override the chart name. diff --git a/helm/vmargs_test.yaml b/helm/vmargs_test.yaml new file mode 100644 index 0000000000..a99b5f31a9 --- /dev/null +++ b/helm/vmargs_test.yaml @@ -0,0 +1,54 @@ +suite: test vm.args wiring +templates: + - vm-args-configmap.yaml + - deployment.yaml +tests: + - it: renders the vm.args ConfigMap with +Q by default + template: vm-args-configmap.yaml + asserts: + - isKind: + of: ConfigMap + - matchRegex: + path: data["vm.args"] + pattern: "\\+Q 1048576" + + - it: sets RELEASE_VM_ARGS and mounts vm-args by default + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: RELEASE_VM_ARGS + value: /etc/logflare/vm.args + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: vm-args + mountPath: /etc/logflare/vm.args + subPath: vm.args + readOnly: true + - contains: + path: spec.template.spec.volumes + content: + name: vm-args + configMap: + name: RELEASE-NAME-logflare-vm-args + + - it: renders no ConfigMap when vmArgs is empty + set: + vmArgs: "" + template: vm-args-configmap.yaml + asserts: + - hasDocuments: + count: 0 + + - it: omits RELEASE_VM_ARGS when vmArgs is empty + set: + vmArgs: "" + template: deployment.yaml + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: RELEASE_VM_ARGS + value: /etc/logflare/vm.args From 7df705687a9dc188dcd2ac06c3195616bec584d6 Mon Sep 17 00:00:00 2001 From: David Whittington Date: Fri, 24 Jul 2026 14:00:43 -0500 Subject: [PATCH 08/11] feat(helm): replace reloader toggle with generic deploymentAnnotations (#3736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(helm): support custom vm.args via RELEASE_VM_ARGS Adds an opt-in `vmArgs` value rendered into a ConfigMap, mounted at /etc/logflare/vm.args, and wired via RELEASE_VM_ARGS. Defaults to a set including `+Q 1048576`, which caps the BEAM's preallocated port table — needed on Kubernetes, where containerd runs pods with nofile=infinity and the VM would otherwise size the table from that (~2GB RSS at boot). Mounted with subPath so it coexists with the cert-files mount under /etc/logflare. A checksum/vm-args pod annotation rolls the Deployment when the args change. Set vmArgs to "" to fall back to the image's baked-in vm.args. Adds helm-unittest coverage. * feat(helm): replace Stakater-specific reloader toggle with generic deploymentAnnotations Per review on #3728: the `logflare.reloader` boolean hard-coded a Stakater- specific annotation, which is too opinionated for a public chart. Replace it with a generic top-level `deploymentAnnotations` map (mirroring podAnnotations) that renders onto the Deployment's own metadata.annotations. This keeps the chart neutral about the reload mechanism — opt into Reloader with `deploymentAnnotations: { reloader.stakater.com/auto: "true" }` — and fills a real gap: there was no hook for Deployment-level annotations (podAnnotations only reaches the pod template). Updates the helm-unittest cases and README. --- helm/README.md | 2 +- helm/deployment_test.yaml | 9 +++++---- helm/templates/deployment.yaml | 4 ++-- helm/values.yaml | 10 +++++++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/helm/README.md b/helm/README.md index c71091825f..f16887e191 100644 --- a/helm/README.md +++ b/helm/README.md @@ -160,7 +160,7 @@ You can list multiple Secret names in `secretRefs` (e.g. one per `ExternalSecret | `logflare.secretRefs` | see [Loading secrets](#loading-secrets) | | | `logflare.certFilesSecret` | — | Name of a Secret whose keys are cert filenames; mounted as files. See [Certificate files](#certificate-files) | | `logflare.certFilesMountPath` | `DB_SSL_*_PATH`, `LOGFLARE_TLS_*_PATH` | Mount path for `certFilesSecret`; the chart points the cert path env vars here | -| `logflare.reloader` | — | When `true`, adds the Stakater Reloader annotation so the Deployment rolls on ConfigMap/Secret changes | +| `deploymentAnnotations` | — | Annotations on the Deployment object. Set `reloader.stakater.com/auto: "true"` to roll pods on ConfigMap/Secret changes (requires Stakater Reloader) | | `logflare.extraConfig` | (any) | Map of non-secret env vars rendered verbatim into the ConfigMap | See `values.yaml` for the full set of generic chart values (image, service, ingress, resources, autoscaling, etc.). diff --git a/helm/deployment_test.yaml b/helm/deployment_test.yaml index 60fcf8c0b5..c004333b4e 100644 --- a/helm/deployment_test.yaml +++ b/helm/deployment_test.yaml @@ -134,15 +134,16 @@ tests: value: /etc/logflare/certs/cert.pem template: deployment.yaml - - it: should add the Reloader annotation only when enabled + - it: should set no Deployment annotations by default asserts: - notExists: - path: metadata.annotations["reloader.stakater.com/auto"] + path: metadata.annotations template: deployment.yaml - - it: should add the Reloader annotation when reloader is true + - it: should render deploymentAnnotations on the Deployment when set set: - logflare.reloader: true + deploymentAnnotations: + reloader.stakater.com/auto: "true" asserts: - equal: path: metadata.annotations["reloader.stakater.com/auto"] diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 340f02e0f6..2070d9483c 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -2,9 +2,9 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "logflare.fullname" . }} - {{- if .Values.logflare.reloader }} + {{- with .Values.deploymentAnnotations }} annotations: - reloader.stakater.com/auto: "true" + {{- toYaml . | nindent 4 }} {{- end }} labels: {{- include "logflare.labels" . | nindent 4 }} diff --git a/helm/values.yaml b/helm/values.yaml index ef54a91c65..5acb1b5458 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -56,9 +56,6 @@ logflare: certFilesSecret: "" certFilesMountPath: /etc/logflare/certs - # Roll the Deployment automatically when its ConfigMap or referenced Secrets - # change. Requires the Stakater Reloader controller to be installed. - reloader: false # Free-form non-secret environment variables rendered verbatim into the # ConfigMap. Use this for configuration not modeled as first-class values @@ -123,6 +120,13 @@ serviceAccount: # If not set and create is true, a name is generated using the fullname template. name: "" +# Annotations for the Deployment object itself (not the pods). Use this to +# drive controllers that watch the Deployment — e.g. Stakater Reloader, to roll +# pods when a referenced ConfigMap/Secret changes: +# deploymentAnnotations: +# reloader.stakater.com/auto: "true" +deploymentAnnotations: {} + # This is for setting Kubernetes Annotations to a Pod. # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ podAnnotations: {} From 90bcebed2fb6de506a323e141fca998bcda81274 Mon Sep 17 00:00:00 2001 From: Jared Patterson Date: Sat, 25 Jul 2026 08:26:28 +1200 Subject: [PATCH 09/11] feat(helm): add optional ESO ExternalSecret templates (#3729) Render ExternalSecret resources directly from the chart when externalSecrets.enabled is true, so ESO users can sync secrets without hand-writing the ExternalSecrets. Off by default, so the chart still renders no Secret and takes no dependency on ESO CRDs for anyone who does not use it. The (Cluster)SecretStore name/kind and every remote path live under a single externalSecrets block in values. Each entry maps keys to data[].remoteRef (secretKey is the key, remoteRef.key is remotePrefix/key), with verbatim data/dataFrom passthrough for cases keys cannot express. The block is ESO-specific by design, leaving room for sibling provider blocks (e.g. Vault) later. Adds helm-unittest coverage and README docs, and bumps the chart to 0.2.0. --- helm/Chart.yaml | 2 +- helm/README.md | 61 +++++++++++ helm/externalsecret_test.yaml | 163 +++++++++++++++++++++++++++++ helm/templates/externalsecret.yaml | 46 ++++++++ helm/values.yaml | 54 ++++++++++ 5 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 helm/externalsecret_test.yaml create mode 100644 helm/templates/externalsecret.yaml diff --git a/helm/Chart.yaml b/helm/Chart.yaml index bba6fb81aa..c6458131a0 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 +version: 0.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/helm/README.md b/helm/README.md index f16887e191..20591eae3c 100644 --- a/helm/README.md +++ b/helm/README.md @@ -129,6 +129,67 @@ logflare: You can list multiple Secret names in `secretRefs` (e.g. one per `ExternalSecret`/upstream secret-store path) — all of their keys are injected together. +#### Letting the chart render the ExternalSecrets (optional) + +If you already run ESO, the chart can render the `ExternalSecret` objects for you instead of you hand-writing them, so all of the deployment configuration lives in one place. This is opt-in via `externalSecrets.enabled` and requires ESO (and its CRDs) to be installed in the cluster. When it is disabled (the default) the chart renders no `ExternalSecret` and the manual flow above still applies. + +Point it at an existing `(Cluster)SecretStore` and list the secrets to sync. Each entry produces one `ExternalSecret`, and therefore one Kubernetes `Secret`, whose name you then reference in `logflare.secretRefs` (for env vars) or mount as files (for certificates): + +```yaml +externalSecrets: + enabled: true + secretStore: + name: my-secret-store # an existing ClusterSecretStore / SecretStore + kind: ClusterSecretStore + secrets: + - name: logflare-secrets # env-var secrets, injected via secretRefs below + remotePrefix: secret/logflare + keys: + - PHX_SECRET_KEY_BASE + - PHX_LIVE_VIEW_SIGNING_SALT + - LOGFLARE_DB_ENCRYPTION_KEY + - DB_PASSWORD + - GOOGLE_SERVICE_ACCOUNT + - name: logflare-cert-files # cert/key files, mounted as a volume + remotePrefix: secret/logflare + keys: + - db-server-ca.pem + - db-client-cert.pem + - db-client-key.pem + +logflare: + secretRefs: + - logflare-secrets + +# Mount the cert-file Secret with the chart's generic volume hooks: +volumes: + - name: logflare-cert-files + secret: + secretName: logflare-cert-files +volumeMounts: + - name: logflare-cert-files + mountPath: /etc/logflare/certs + readOnly: true +``` + +`remotePrefix` is prepended to each key, so a key of `DB_PASSWORD` reads from `secret/logflare/DB_PASSWORD` and lands in the Secret under the identical key name `DB_PASSWORD`. Omit `remotePrefix` to use each key as the remote reference verbatim. + +When the backend paths do not map one-to-one to key names, drop to the verbatim `data` and `dataFrom` passthroughs on an entry (they map straight onto the `ExternalSecret` spec), for example to extract a whole bundle: + +```yaml +externalSecrets: + enabled: true + secretStore: + name: my-secret-store + secrets: + - name: logflare-secrets + dataFrom: + - extract: + key: secret/logflare/all +``` + +Per-entry `refreshInterval` and `creationPolicy` overrides are also supported; see `values.yaml` for the full set of fields. This block is ESO-specific by design, so support for other providers (for example a HashiCorp Vault Agent integration) can be added as a sibling block without disturbing it. + ## Configurable values | `values.yaml` key | Env var | Notes | diff --git a/helm/externalsecret_test.yaml b/helm/externalsecret_test.yaml new file mode 100644 index 0000000000..be3726112d --- /dev/null +++ b/helm/externalsecret_test.yaml @@ -0,0 +1,163 @@ +suite: test external secrets +templates: + - externalsecret.yaml +tests: + - it: should render no ExternalSecret by default + asserts: + - hasDocuments: + count: 0 + + - it: should render one ExternalSecret per entry when enabled + set: + externalSecrets: + enabled: true + secretStore: + name: my-store + kind: ClusterSecretStore + refreshInterval: 1h + secrets: + - name: logflare-secrets + remotePrefix: secret/logflare + keys: + - PHX_SECRET_KEY_BASE + - DB_PASSWORD + - name: logflare-cert-files + remotePrefix: secret/logflare + keys: + - db-server-ca.pem + asserts: + - hasDocuments: + count: 2 + - containsDocument: + kind: ExternalSecret + apiVersion: external-secrets.io/v1 + name: logflare-secrets + documentIndex: 0 + - equal: + path: spec.secretStoreRef.name + value: my-store + documentIndex: 0 + - equal: + path: spec.secretStoreRef.kind + value: ClusterSecretStore + documentIndex: 0 + - equal: + path: spec.target.name + value: logflare-secrets + documentIndex: 0 + - equal: + path: spec.target.creationPolicy + value: Owner + documentIndex: 0 + - equal: + path: spec.refreshInterval + value: 1h + documentIndex: 0 + - equal: + path: spec.data[0].secretKey + value: PHX_SECRET_KEY_BASE + documentIndex: 0 + - equal: + path: spec.data[0].remoteRef.key + value: secret/logflare/PHX_SECRET_KEY_BASE + documentIndex: 0 + - equal: + path: metadata.name + value: logflare-cert-files + documentIndex: 1 + - equal: + path: spec.data[0].remoteRef.key + value: secret/logflare/db-server-ca.pem + documentIndex: 1 + + - it: should use the raw key when remotePrefix is unset + set: + externalSecrets: + enabled: true + secretStore: + name: my-store + secrets: + - name: logflare-secrets + keys: + - DB_PASSWORD + asserts: + - equal: + path: spec.data[0].remoteRef.key + value: DB_PASSWORD + documentIndex: 0 + + - it: should default the store kind to ClusterSecretStore + set: + externalSecrets: + enabled: true + secretStore: + name: my-store + secrets: + - name: logflare-secrets + keys: + - DB_PASSWORD + asserts: + - equal: + path: spec.secretStoreRef.kind + value: ClusterSecretStore + documentIndex: 0 + + - it: should support a SecretStore kind override + set: + externalSecrets: + enabled: true + secretStore: + name: my-store + kind: SecretStore + secrets: + - name: logflare-secrets + keys: + - DB_PASSWORD + asserts: + - equal: + path: spec.secretStoreRef.kind + value: SecretStore + documentIndex: 0 + + - it: should pass dataFrom through verbatim + set: + externalSecrets: + enabled: true + secretStore: + name: my-store + secrets: + - name: logflare-bundle + dataFrom: + - extract: + key: prod/logflare/bundle + asserts: + - hasDocuments: + count: 1 + - equal: + path: spec.dataFrom[0].extract.key + value: prod/logflare/bundle + documentIndex: 0 + + - it: should fail when enabled but the store name is empty + set: + externalSecrets: + enabled: true + secrets: + - name: logflare-secrets + keys: + - DB_PASSWORD + asserts: + - failedTemplate: + errorMessage: "externalSecrets.enabled is true but externalSecrets.secretStore.name is empty; set externalSecrets.secretStore.name to your (Cluster)SecretStore name" + + - it: should fail when an entry has no keys, data, or dataFrom + set: + externalSecrets: + enabled: true + secretStore: + name: my-store + secrets: + - name: logflare-secrets + asserts: + - failedTemplate: + errorMessage: 'externalSecrets.secrets entry "logflare-secrets" must set at least one of keys, data, or dataFrom' diff --git a/helm/templates/externalsecret.yaml b/helm/templates/externalsecret.yaml new file mode 100644 index 0000000000..22445c4175 --- /dev/null +++ b/helm/templates/externalsecret.yaml @@ -0,0 +1,46 @@ +{{- if .Values.externalSecrets.enabled }} +{{- $top := . }} +{{- $store := .Values.externalSecrets.secretStore }} +{{- if not $store.name }} +{{- fail "externalSecrets.enabled is true but externalSecrets.secretStore.name is empty; set externalSecrets.secretStore.name to your (Cluster)SecretStore name" }} +{{- end }} +{{- range $secret := .Values.externalSecrets.secrets }} +{{- if not $secret.name }} +{{- fail "each externalSecrets.secrets entry must set a name" }} +{{- end }} +{{- if not (or $secret.keys $secret.data $secret.dataFrom) }} +{{- fail (printf "externalSecrets.secrets entry %q must set at least one of keys, data, or dataFrom" $secret.name) }} +{{- end }} +{{- $prefix := $secret.remotePrefix | default "" | trimSuffix "/" }} +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ $secret.name }} + labels: + {{- include "logflare.labels" $top | nindent 4 }} +spec: + refreshInterval: {{ $secret.refreshInterval | default $top.Values.externalSecrets.refreshInterval | quote }} + secretStoreRef: + kind: {{ $store.kind | default "ClusterSecretStore" }} + name: {{ $store.name | quote }} + target: + name: {{ $secret.name }} + creationPolicy: {{ $secret.creationPolicy | default "Owner" }} + {{- if or $secret.keys $secret.data }} + data: + {{- range $key := ($secret.keys | default (list)) }} + - secretKey: {{ $key | quote }} + remoteRef: + key: {{ if $prefix }}{{ printf "%s/%s" $prefix $key | quote }}{{ else }}{{ $key | quote }}{{ end }} + {{- end }} + {{- with $secret.data }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + {{- with $secret.dataFrom }} + dataFrom: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 5acb1b5458..72fff5e336 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -102,6 +102,60 @@ vmArgs: | -kernel prevent_overlapping_partitions false -kernel net_ticktime 45 +# External Secrets Operator (ESO) integration. When enabled, the chart renders +# ExternalSecret resources so ESO syncs secrets from your backend (AWS SSM, +# Vault, GCP Secret Manager, etc., resolved via the referenced SecretStore) into +# the Kubernetes Secrets this chart consumes. Requires ESO and its CRDs to be +# installed in the cluster. Off by default: when disabled the chart renders no +# ExternalSecret and you provision the Secrets in logflare.secretRefs yourself. +# +# This is one secret-provider integration. Others (e.g. a HashiCorp Vault Agent +# block) can be added alongside it without changing this one. +externalSecrets: + enabled: false + + # The (Cluster)SecretStore every generated ExternalSecret references. ESO + # resolves the actual backend (SSM, Vault, ...) from this store, so the store + # must already exist in the cluster. + secretStore: + # Name of the store. Required when externalSecrets.enabled is true. + name: "" + # ClusterSecretStore (cluster-scoped) or SecretStore (namespaced). + kind: ClusterSecretStore + + # Default re-sync interval for every generated ExternalSecret. Override per + # entry with secrets[].refreshInterval. + refreshInterval: 1h + + # ExternalSecrets to generate. Each entry produces one ExternalSecret and, via + # ESO, one Kubernetes Secret named `name`. Add env-var Secrets to + # logflare.secretRefs; mount file Secrets via volumes / volumeMounts. + secrets: [] + # - name: logflare-secrets + # # Prepended to each key's remoteRef.key below. Omit for raw keys. + # remotePrefix: secret/logflare + # # Each key becomes a Secret entry whose key name IS the key, pulled from + # # /. For env-var Secrets these must match the env vars + # # Logflare reads. + # keys: + # - PHX_SECRET_KEY_BASE + # - DB_PASSWORD + # - name: logflare-cert-files + # remotePrefix: secret/logflare + # keys: + # - db-server-ca.pem + # - cert.pem + # # Optional per-entry overrides: + # # refreshInterval: 30m + # # creationPolicy: Owner + # # Advanced verbatim passthrough, merged after `keys` (use for bundle + # # extraction or custom remoteRef options): + # # data: + # # - secretKey: FOO + # # remoteRef: { key: path/to/foo } + # # dataFrom: + # # - extract: { key: prod/logflare/bundle } + # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ imagePullSecrets: [] # This is to override the chart name. From 1c12686a1611fe499f6f49a3b5506a9243bd6b12 Mon Sep 17 00:00:00 2001 From: Jared Patterson Date: Sat, 25 Jul 2026 08:29:23 +1200 Subject: [PATCH 10/11] Apply suggestion from @depthfirst-app[bot] Co-authored-by: depthfirst-app[bot] <184448029+depthfirst-app[bot]@users.noreply.github.com> --- .github/workflows/helm-chart.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml index 8e39a16efa..bb91b17352 100644 --- a/.github/workflows/helm-chart.yml +++ b/.github/workflows/helm-chart.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - name: Set up Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 - name: Install helm-unittest plugin run: helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest From 2fc8b3931218b87887c24493c346217f04ab9be4 Mon Sep 17 00:00:00 2001 From: Jared Patterson Date: Sat, 25 Jul 2026 08:51:44 +1200 Subject: [PATCH 11/11] test(helm): fix unittest suites for vm-args and cert-file volumes List every template deployment.yaml includes in each suite's templates key, so helm-unittest compiles configmap.yaml and vm-args-configmap.yaml and the checksum includes resolve instead of erroring. Rewrite the cert-file volume and mount assertions to use contains/notContains keyed on the volume name rather than a fixed [0] index, since vmArgs is enabled by default and its vm-args volume now occupies index 0. --- helm/deployment_test.yaml | 34 +++++++++++++++++++++++----------- helm/vmargs_test.yaml | 1 + 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/helm/deployment_test.yaml b/helm/deployment_test.yaml index c004333b4e..81f4dca1cb 100644 --- a/helm/deployment_test.yaml +++ b/helm/deployment_test.yaml @@ -2,6 +2,7 @@ suite: test deployment kind templates: - deployment.yaml - configmap.yaml + - vm-args-configmap.yaml tests: - it: should render the correct deployment Kind asserts: @@ -95,8 +96,17 @@ tests: - it: should not mount cert files or set cert paths by default asserts: - - notExists: + - notContains: path: spec.template.spec.volumes + content: + name: cert-files + any: true + template: deployment.yaml + - notContains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: cert-files + any: true template: deployment.yaml - notContains: path: spec.template.spec.containers[0].env @@ -109,17 +119,19 @@ tests: set: logflare.certFilesSecret: logflare-cert-files asserts: - - equal: - path: spec.template.spec.volumes[0].name - value: cert-files - template: deployment.yaml - - equal: - path: spec.template.spec.volumes[0].secret.secretName - value: logflare-cert-files + - contains: + path: spec.template.spec.volumes + content: + name: cert-files + secret: + secretName: logflare-cert-files template: deployment.yaml - - equal: - path: spec.template.spec.containers[0].volumeMounts[0].mountPath - value: /etc/logflare/certs + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: cert-files + mountPath: /etc/logflare/certs + readOnly: true template: deployment.yaml - contains: path: spec.template.spec.containers[0].env diff --git a/helm/vmargs_test.yaml b/helm/vmargs_test.yaml index a99b5f31a9..5dcd0d1e15 100644 --- a/helm/vmargs_test.yaml +++ b/helm/vmargs_test.yaml @@ -2,6 +2,7 @@ suite: test vm.args wiring templates: - vm-args-configmap.yaml - deployment.yaml + - configmap.yaml tests: - it: renders the vm.args ConfigMap with +Q by default template: vm-args-configmap.yaml