-
Notifications
You must be signed in to change notification settings - Fork 88
feat(helm): add initial Helm chart and CI workflow #3719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||
|
|
||||||
| - name: Install helm-unittest plugin | ||||||
| run: helm plugin install --verify=false https://github.com/helm-unittest/helm-unittest | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Severity: MEDIUM The 💡 Fix SuggestionSuggestion: Remove the
Suggested change
|
||||||
|
|
||||||
| - 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" | ||||||
| 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/ |
| 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" |
| 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`). |
| 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 |
There was a problem hiding this comment.
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-helmaction is pinned to the mutable tagv4rather than an immutable full-length commit SHA. If thev4tag 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-helmaction to an immutable full-length commit SHA instead of the mutablev4tag. This prevents a tag move or supply-chain compromise from silently replacing the action with malicious code. The current commit SHA behind thev4tag is1a275c3b69536ee54be43f2070a358922e12c8d4. Keep the# v4comment so the human-readable version is still visible. Additionally, note thatactions/checkout@v6on line 17 has the same problem and should also be pinned to its immutable SHA.