Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/helm-chart.yml
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

The azure/setup-helm action is pinned to the mutable tag v4 rather than an immutable full-length commit SHA. If the v4 tag is moved — e.g. after a supply chain compromise of the upstream repo — a malicious action version would execute with full CI runner privileges and access to all workflow secrets.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Pin the azure/setup-helm action to an immutable full-length commit SHA instead of the mutable v4 tag. This prevents a tag move or supply-chain compromise from silently replacing the action with malicious code. The current commit SHA behind the v4 tag is 1a275c3b69536ee54be43f2070a358922e12c8d4. Keep the # v4 comment so the human-readable version is still visible. Additionally, note that actions/checkout@v6 on line 17 has the same problem and should also be pinned to its immutable SHA.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Severity: MEDIUM

The --verify=false flag explicitly disables cryptographic signature verification when downloading the helm-unittest plugin from GitHub. If the upstream repository or release artifacts are compromised, a malicious plugin binary would be installed and executed with full CI runner privileges without any integrity check.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Remove the --verify=false flag from the helm plugin install command so that Helm performs its default cryptographic signature verification when downloading and installing the helm-unittest plugin. This ensures the integrity of the plugin binary is validated before execution in the CI runner.

⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.

Suggested change
run: helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest
run: helm plugin install 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:-<none>}"
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"
23 changes: 23 additions & 0 deletions helm/.helmignore
Original file line number Diff line number Diff line change
@@ -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/
24 changes: 24 additions & 0 deletions helm/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: v2
name: logflare
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.
#
# 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.47.2"
178 changes: 178 additions & 0 deletions helm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# 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

**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:
secretRefs:
- logflare-secrets
```

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`.

Most deployments need at least these env vars, so your Secret(s) should contain these keys:

| 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 |

### Creating the Secret yourself

```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:
secretRefs:
- logflare-secrets
```

### 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`, 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, with keys already named to match what Logflare expects:

```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
```

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. **List the Secret's name in `logflare.secretRefs`:**

```yaml
logflare:
secretRefs:
- logflare-secrets
```

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
```

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

| `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.secretRefs` | 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`).
53 changes: 53 additions & 0 deletions helm/configmap_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
suite: test backend wiring
templates:
- configmap.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

- 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

- 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
Loading
Loading