diff --git a/.gitignore b/.gitignore index 68d779f2..1662a469 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,13 @@ sdk/highlight-wordpress/highlight-io/highlight.js # .NET build outputs **/bin/ **/obj/ + +# Tamp build scripts (HOL-54) — un-ignore the root /build/ dir that holds +# Build.cs + Build.csproj. The **/build rule above still hides nested build/ +# trees inside packages, and **/bin/ + **/obj/ still hide compile output +# inside /build/ itself. +!/build/ +!/build/** + +# Tamp's own artifacts/ folder (TRX, coverage, publish output) +/artifacts/ diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 00000000..90d0a3ce --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,284 @@ +using Tamp; +using Tamp.NetCli.V10; +using Tamp.Yarn.V4; +using Tamp.Turbo.V2; +using Tamp.Docker.V27; +using Tamp.Helm.V3; +using Tamp.Http; +using Tamp.GraphQLCodegen.V5; +using Tamp.Coverlet.V6; +using Tamp.ReportGenerator.V5; +using Tamp.Syft; +using Tamp.Grype; +using Tamp.TruffleHog.V3; + +class Build : TampBuild +{ + public static int Main(string[] args) => Execute(args); + + [Parameter("Build configuration (Debug|Release)")] + Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Parameter("Container registry for QA push")] + string Registry = "localhost:32000"; + + [Parameter("QA hostname (no trailing slash)")] + string QaUrl = "https://holdfast.brewingcoder.com"; + + [Parameter("Override the computed image tag (defaults to short git SHA)")] + string? ImageTagOverride = null; + + // HoldFast is a multi-solution monorepo (SDK + e2e scaffolds also carry + // .sln/.slnx files), so the subtree search would be ambiguous. Pin explicitly. + [Solution("src/dotnet/HoldFast.Backend.slnx")] readonly Solution Solution = null!; + [GitRepository] readonly GitRepository Git = null!; + + [FromPath("yarn")] readonly Tool YarnTool = null!; + [FromPath("helm")] readonly Tool HelmTool = null!; + // Compliance + coverage tools are operator-installed (one tool per axis; + // see Tamp's Module Catalog). Marked Optional so the target surface + // enumerates on machines without them — invocation will surface a + // targeted error then, not a global injection failure. + [FromPath("syft", Optional = true)] readonly Tool SyftTool = null!; + [FromPath("grype", Optional = true)] readonly Tool GrypeTool = null!; + [FromPath("trufflehog", Optional = true)] readonly Tool TruffleHogTool = null!; + [FromPath("reportgenerator", Optional = true)] readonly Tool ReportGeneratorTool = null!; + [FromNodeModules("turbo")] readonly Tool TurboTool = null!; + [FromNodeModules("graphql-codegen", Optional = true)] readonly Tool GraphQLCodegenTool = null!; + + AbsolutePath Artifacts => RootDirectory / "artifacts"; + AbsolutePath PublishDir => Artifacts / "publish" / "HoldFast.Api"; + AbsolutePath CoverageDir => Artifacts / "coverage"; + AbsolutePath CoverageReportDir => Artifacts / "coverage-report"; + AbsolutePath Sbom => Artifacts / $"holdfast-{Version}.cdx.json"; + AbsolutePath HelmChart => RootDirectory / "infra" / "helm" / "holdfast"; + + // Image tag = short git SHA (CLI override wins). Canonical version lives + // in Chart.yaml.appVersion. GitVersion-derived semver is the future state + // but Tamp.GitVersion.V6 0.1.1 doesn't ship the [GitVersion] injection + // attribute yet — friction filed to airm5; revisit when that lands. + string Version => ImageTagOverride ?? Git!.Commit[..7]; + string ImageTag => Version; + string LocalImageRef => $"holdfast-backend-dotnet:{ImageTag}"; + string RegistryImageRef => $"{Registry}/holdfast-backend-dotnet:{ImageTag}"; + + Target Info => _ => _ + .Executes(() => + { + Console.WriteLine("HoldFast build via Tamp"); + Console.WriteLine($" Configuration: {Configuration}"); + Console.WriteLine($" Solution: {Solution?.Path}"); + Console.WriteLine($" Root: {RootDirectory}"); + Console.WriteLine($" Artifacts: {Artifacts}"); + Console.WriteLine($" Git branch: {Git?.Branch}"); + Console.WriteLine($" Git commit: {Git?.Commit}"); + Console.WriteLine($" Version: {Version}"); + Console.WriteLine($" Image tag: {ImageTag}"); + Console.WriteLine($" Registry ref: {RegistryImageRef}"); + Console.WriteLine($" QA URL: {QaUrl}"); + }); + + Target Restore => _ => _ + .Executes(() => DotNet.Restore(s => s + .SetProject(Solution.Path))); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => DotNet.Build(s => s + .SetProject(Solution.Path) + .SetConfiguration(Configuration) + .SetNoRestore(true))); + + // NetCli.V10 1.0.9+ auto-expands LogFileName → LogFilePrefix in solution + // mode, so this produces one TRX file per test assembly. + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => DotNet.Test(s => s + .SetProject(Solution.Path) + .SetConfiguration(Configuration) + .SetNoBuild(true) + .SetNoRestore(true) + .SetResultsDirectory(Artifacts / "test-results") + .AddLogger("trx;LogFileName=test-results.trx"))); + + // Coverage variant of Test — collects XPlat Code Coverage via the + // standard data collector. Coverlet config built via the satellite's + // Configure(...) helper, then handed to dotnet test as a runsettings + // file. Kept separate from Test so the fast Ci path doesn't pay + // coverage overhead on every run. + Target CoverageTest => _ => _ + .DependsOn(Compile) + .Executes(() => + { + var runSettings = Artifacts / "coverlet.runsettings"; + System.IO.Directory.CreateDirectory(Artifacts); + var xml = Coverlet.Configure(s => s + .AddFormat(CoverletFormat.OpenCover) + .AddExclude("[xunit.*]*") + .AddExclude("[*.Tests]*") + .SetUseSourceLink(true)).ToRunSettingsXml(); + System.IO.File.WriteAllText(runSettings, xml); + + return DotNet.Test(s => s + .SetProject(Solution.Path) + .SetConfiguration(Configuration) + .SetNoBuild(true) + .SetNoRestore(true) + .SetResultsDirectory(CoverageDir) + .SetSettings(runSettings) + .AddLogger("trx;LogFileName=test-results.trx")); + }); + + Target CoverageReport => _ => _ + .DependsOn(CoverageTest) + .Executes(() => ReportGenerator.Run(ReportGeneratorTool, s => s + .AddReport(CoverageDir / "**" / "coverage.opencover.xml") + .SetTargetDir(CoverageReportDir) + .AddReportType("Html") + .AddReportType("Badges") + .AddReportType("MarkdownSummaryGithub"))); + + // CleanArtifacts(): framework-provided safe wipe — Solution.Projects only, + // self-deletion guarded. Never use RootDirectory.GlobDirectories("**/bin") + // — that's the friction-#12 footgun. + Target Clean => _ => _ + .Executes(() => CleanArtifacts()); + + Target Publish => _ => _ + .DependsOn(Compile) + .Executes(() => DotNet.Publish(s => s + .SetProject(RootDirectory / "src" / "dotnet" / "src" / "HoldFast.Api" / "HoldFast.Api.csproj") + .SetConfiguration(Configuration) + .SetOutput(PublishDir) + .SetNoBuild(true) + .SetNoRestore(true))); + + // ── Frontend (Yarn Berry 4.x + Turbo + Vite) ────────────────────── + + Target YarnInstall => _ => _ + .Executes(() => Yarn.Install(YarnTool, s => s.SetImmutable(true))); + + // Regenerate GraphQL TypeScript types from src/backend/private-graph schema. + // Generated files are checked in (src/frontend/src/graph/generated/) so + // day-to-day frontend work doesn't have to wait on codegen — this target + // runs on demand when *.gql or schema.graphqls drift. + Target FrontendCodegen => _ => _ + .DependsOn(YarnInstall) + .Executes(() => GraphQLCodegen.Generate(GraphQLCodegenTool, s => s + .SetWorkingDirectory(RootDirectory / "src" / "frontend") + .SetConfig("codegen.yml"))); + + // Workspace-local turbo only exists after YarnInstall populates + // node_modules/.bin/turbo, so this DependsOn is mandatory. + Target FrontendBuild => _ => _ + .DependsOn(YarnInstall) + .Executes(() => Turbo.Run(TurboTool, s => s + .SetWorkingDirectory(RootDirectory) + .AddTask("build:fast") + .AddFilter("@holdfast-io/frontend..."))); + + // ── Docker ────────────────────────────────────────────────────── + + // Docker.V27 0.3.x routes Build through `docker buildx build`, so the + // Dockerfile's `RUN --mount=type=cache` directives work. Two tags so the + // local-shorthand reference and the registry-prefixed reference both land. + Target DockerBuildBackend => _ => _ + .Executes(() => Docker.Build(s => s + .SetContext(RootDirectory) + .SetDockerfile(RootDirectory / "infra" / "docker" / "backend-dotnet.Dockerfile") + .AddTag(LocalImageRef) + .AddTag(RegistryImageRef))); + + // Push the registry-prefixed image to the lab registry. ARC runner has its + // ~/.docker/config.json populated for localhost:32000 (plain HTTP, daemon- + // level insecure-registries setting); no Docker.Login call needed. + Target DockerPush => _ => _ + .DependsOn(DockerBuildBackend) + .Executes(() => Docker.Push(s => s + .SetImage(RegistryImageRef))); + + // ── Supply chain ───────────────────────────────────────────────── + + // CycloneDX SBOM for the whole repo. Excludes the transitively-vendored + // node_modules / bin / obj noise so the SBOM reflects first-order deps + // an operator actually has to defend. Output is consumed by CveGate. + Target SbomScan => _ => _ + .Executes(() => Syft.Scan(SyftTool, s => s + .SetDirectorySource(RootDirectory) + .SetSourceName("HoldFast") + .SetSourceVersion(Version) + .AddOutputCycloneDxJson(Sbom) + .AddExcludes("**/node_modules/**", "**/bin/**", "**/obj/**"))); + + // CVE gate — reads the SBOM, hits NVD + GitHub Advisory DB + KEV, applies + // EPSS-weighted composite risk scoring. Fails the build on >= high + // severity. Adopters tune severity via --fail-on on the CLI. + Target CveGate => _ => _ + .DependsOn(SbomScan) + .Executes(() => Grype.Scan(GrypeTool, s => s + .SetSbomSource(Sbom) + .AddOutputJson() + .SetOutputFile(Artifacts / "vulns.json") + .SetFailOn("high") + .SetSortBy("risk") + .SetByCve(true))); + + // Secret scan — TruffleHog over the filesystem. Verified-only so unverified + // pattern matches (often false positives in test fixtures) don't flap the + // build. Run as part of Compliance, not Ci, because verification hits live + // endpoints (slower than the no-network analyzers). + Target SecretScan => _ => _ + .Executes(() => TruffleHog.Filesystem(TruffleHogTool, s => s + .AddPath(RootDirectory) + .SetOnlyVerified(true) + .SetFail(true))); + + // Aggregate compliance gate — `dotnet tamp Compliance` runs the full + // supply-chain triplet for a release-prep snapshot. + Target Compliance => _ => _ + .DependsOn(SbomScan, CveGate, SecretScan); + + // ── Deploy ────────────────────────────────────────────────────── + + // Deploy the chart to the lab cluster. helm upgrade --install is idempotent; + // --atomic rolls back automatically on a failed rollout. + // Atomic disabled for now so a failed deploy leaves the cluster state + // around for kubectl inspection. Re-enable once the chart has a few + // green runs under it. Timeout bumped to 10m to give cold image pulls + // (TimescaleDB-HA is 1.73 GB) headroom on first deploy to each node. + Target DeployQa => _ => _ + .DependsOn(DockerPush) + .Executes(() => Helm.Upgrade(HelmTool, s => s + .SetRelease("holdfast") + .SetNamespace("holdfast") + .SetCreateNamespace(true) + .SetChart(HelmChart) + .AddValuesFile(HelmChart / "values.lab.yaml") + .SetValue("image.tag", ImageTag) + .SetWait(true) + .SetAtomic(false) + .SetTimeout(TimeSpan.FromMinutes(10)))); + + // Post-deploy smoke probe — polls /health until it returns 200 or the + // timeout elapses. HttpProbe handles transient HttpRequestExceptions and + // per-request timeouts as expected during pod warmup. + // Backend's MapHealthChecks lands on /health (single endpoint, no + // live/ready split). Don't append /live or /ready — those fall through + // the SPA fallback to index.html (HTTP 200) and lie about health. + Target SmokeQa => _ => _ + .DependsOn(DeployQa) + .Executes(async () => await HttpProbe.WaitForHealthy( + url: $"{QaUrl}/health", + timeout: TimeSpan.FromMinutes(2))); + + // ── CI entry ───────────────────────────────────────────────────── + + // `dotnet tamp` (no args) runs the full verification + artifact pipeline. + // Tamp.Core 1.3.0's params Target[] overload makes the fan-out one-liner. + // Compliance (SBOM + CVE + secret scan) is deliberately NOT in Ci — it's + // a release-prep step run separately so iteration on the fast path stays + // fast. `dotnet tamp Compliance` runs it on demand. + Target Ci => _ => _ + .Default() + .DependsOn(Test, Publish, FrontendBuild, DockerBuildBackend); +} diff --git a/build/Build.csproj b/build/Build.csproj new file mode 100644 index 00000000..e0b09cd4 --- /dev/null +++ b/build/Build.csproj @@ -0,0 +1,41 @@ + + + + Exe + net10.0 + HoldFast.Build + enable + latest + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/CHANGELOG-FORK.md b/docs/CHANGELOG-FORK.md index d9703fde..f6489425 100644 --- a/docs/CHANGELOG-FORK.md +++ b/docs/CHANGELOG-FORK.md @@ -1,5 +1,83 @@ # BrewingCoder Fork — Changelog +## 2026-05-12: Build + Deploy on Tamp; Helm Chart for Self-Hosted Operators (HOL-54) + +Replaced ad-hoc `dotnet`/`yarn`/`docker` shell scripting with a Tamp-driven +build pipeline, and added a published-shape Helm chart that operators can +consume directly. Net change: **17 files added, ~1,036 lines** (no deletions). + +### Added: Tamp build pipeline (`build/Build.cs` + `build/Build.csproj`) + +A .NET 10 console project under `/build` defines the entire pipeline as +typed targets against [Tamp](https://github.com/tamp-build/tamp). Surface: + +| Target | Action | +|---|---| +| `Info` | Print configuration / solution / git / image tag context | +| `Clean` | `CleanArtifacts()` — Solution.Projects scope only, no globbing | +| `Restore` | `dotnet restore` on the solution | +| `Compile` | `dotnet build --no-restore` | +| `Test` | `dotnet test --no-build` with per-assembly TRX output | +| `Publish` | `dotnet publish HoldFast.Api → artifacts/publish/HoldFast.Api/` | +| `YarnInstall` | Yarn Berry 4.x workspace install (`--immutable`) | +| `FrontendBuild` | Turbo runs `build:fast` filtered to `@holdfast-io/frontend...` | +| `DockerBuildBackend` | BuildKit-routed `docker build` of the backend image (multi-tag) | +| `DockerPush` | Push registry-prefixed tag to the configured registry | +| `DeployQa` | `helm upgrade --install` against `infra/helm/holdfast/` | +| `SmokeQa` | `HttpProbe.WaitForHealthy(/health)` against the deployed hostname | +| `Ci` | Default — fans out to Test + Publish + FrontendBuild + DockerBuildBackend | + +One-line invocation: `dotnet tamp Ci` or `dotnet tamp SmokeQa --registry `. + +Pinned against the post-Wave-9 Tamp ecosystem (Core 1.3.0, NetCli.V10 1.3.0, +Helm.V3 0.1.0, Http 0.1.1, plus satellite patches). 16 frictions surfaced and +filed during the integration trial; all fixed in coordinated Tamp release +waves. + +### Added: Helm chart at `infra/helm/holdfast/` + +Standard-shape, AGPL-operator-consumable chart for the two-pod deployment. +Renders 7 resources via `helm template`: + +``` +ServiceAccount holdfast +Secret holdfast-postgres (chart-managed OR existingSecret) +ConfigMap holdfast-backend (env: PSQL_*, STORAGE__ANALYTICS, + REACT_APP_FRONTEND_URI, etc.) +Service holdfast-backend :8082 (ClusterIP, named `http`) +Service holdfast-postgres :5432 (ClusterIP, internal only) +Deployment holdfast-backend (1 replica, /health probes) +StatefulSet holdfast-postgres (1 replica, volumeClaimTemplate) +``` + +Operator-facing defaults in `values.yaml` (community-idiomatic). Lab-cluster +overrides in `values.lab.yaml` (storage class, registry, hostnames, existing +Secret reference). README + NOTES.txt document required values and the +`auth.mode=dev → front with a zero-trust proxy` operator guidance. + +### Removed: Nothing + +This change is purely additive. The existing `docker compose -f compose.yml +-f compose.hobby-dotnet.yml up` hobby workflow still works unchanged; Tamp +runs side-by-side. CI/CD workflows remain disabled per the rewrite-stabilization +directive; flipping them to invoke `dotnet tamp Ci` is a follow-up. + +### Cutover criterion (proven against the lab cluster) + +`dotnet tamp SmokeQa --registry registry.home.local` from any developer +laptop or in-cluster ARC runner: + +1. Builds the backend image via BuildKit (multi-stage frontend + backend) +2. Pushes to `registry.home.local/holdfast-backend-dotnet:` +3. `helm upgrade --install` against `infra/helm/holdfast/` with + `values.lab.yaml` overrides +4. Polls `https://holdfast.brewingcoder.com/health` until 200 Healthy + +Steady-state full run: **3.3 seconds** (cache-warm). First run: ~14 minutes +(cold image pull on each node + Postgres init on NFS). + +--- + ## 2026-03-18: Strip Marketing, Lead-Gen, and SaaS Billing Removed all components that served Highlight's SaaS business but have no value for self-hosted deployments. **1,056 files changed — ~82,800 lines deleted.** diff --git a/infra/helm/holdfast/.helmignore b/infra/helm/holdfast/.helmignore new file mode 100644 index 00000000..c696f373 --- /dev/null +++ b/infra/helm/holdfast/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building helm packages. +# See https://helm.sh/docs/chart_template_guide/builtin_objects/ + +.DS_Store +.git/ +.gitignore +.bzr/ +.hg/ +.hgignore +.svn/ +*.swp +*.bak +*.tmp +*.orig +*~ +.project +.idea/ +*.tmproj +.vscode/ + +# Helm +OWNERS diff --git a/infra/helm/holdfast/Chart.yaml b/infra/helm/holdfast/Chart.yaml new file mode 100644 index 00000000..ec5deebf --- /dev/null +++ b/infra/helm/holdfast/Chart.yaml @@ -0,0 +1,31 @@ +apiVersion: v2 +name: holdfast +description: | + HoldFast — self-hosted, AGPL-3.0 observability platform. Session replay, + error monitoring, logging, and distributed tracing in a single .NET 10 + backend with a PostgreSQL (TimescaleDB-HA) data store. Fork of Highlight.io. + +type: application +version: 0.1.0 # chart version — bump on chart shape changes +appVersion: "0.1.0" # app version — bump on backend image tag changes + +home: https://github.com/BrewingCoder/holdfast +sources: + - https://github.com/BrewingCoder/holdfast + +maintainers: + - name: BrewingCoder + email: scott@gscottsingleton.com + +keywords: + - observability + - session-replay + - error-monitoring + - tracing + - logging + - opentelemetry + - self-hosted + +annotations: + category: Observability + licenses: AGPL-3.0-only diff --git a/infra/helm/holdfast/README.md b/infra/helm/holdfast/README.md new file mode 100644 index 00000000..f4ba32fc --- /dev/null +++ b/infra/helm/holdfast/README.md @@ -0,0 +1,94 @@ +# HoldFast Helm Chart + +Self-hosted HoldFast — AGPL-3.0 observability platform — packaged for Kubernetes. + +## TL;DR + +```bash +helm install holdfast oci://ghcr.io/brewingcoder/charts/holdfast \ + --namespace holdfast --create-namespace \ + --set publicUrl=https://holdfast.example.com \ + --set publicGraphUri=https://holdfast.example.com/public \ + --set privateGraphUri=https://holdfast.example.com/private \ + --set collectorOtlpEndpoint=https://holdfast.example.com/otel \ + --set postgres.auth.password=$(openssl rand -base64 24) +``` + +## Architecture + +Two pods. That's the whole deployment. + +| Workload | Image | Role | +|---|---|---| +| `Deployment/-backend` | `holdfast-backend-dotnet` | .NET 10 Kestrel — API + frontend bundle + workers + OTLP receivers (`/otel/v1/{logs,traces,metrics}`) all in one binary | +| `StatefulSet/-postgres` | `timescale/timescaledb-ha:pg16` | Postgres 16 + TimescaleDB extensions, with the full analytics columnar path | + +Stripped from the upstream Highlight.io 9-container architecture: Kafka, Zookeeper, Redis, the OpenTelemetry Collector, the Python predictions service, and the nginx frontend container. All folded into the backend or removed. + +## Requirements + +- Kubernetes 1.27+ +- Helm 3.13+ +- A StorageClass that supports `ReadWriteOnce` (default works fine; lab clusters can override via `postgres.persistence.storageClassName`) +- An ingress / reverse proxy / Cloudflare tunnel pointing at the backend Service on port 8080 (this chart does not create an `Ingress` — operators wire that up to their preferred edge) + +## Required values + +These must be set or the chart won't render usefully (the backend can't compute its own URLs): + +| Key | Example | +|---|---| +| `publicUrl` | `https://holdfast.example.com` | +| `publicGraphUri` | `https://holdfast.example.com/public` | +| `privateGraphUri` | `https://holdfast.example.com/private` | +| `collectorOtlpEndpoint` | `https://holdfast.example.com/otel` | +| `postgres.auth.password` *or* `postgres.auth.existingSecret` | (set, or referenced existing Secret) | + +## Authentication + +**v1 of this chart only supports `auth.mode=dev`** — the backend runs with no in-app authentication. **Do not expose to anything beyond a trusted network without fronting it with a zero-trust proxy** (Cloudflare Access, Authelia, oauth2-proxy, etc). + +`auth.mode=enterprise` (in-app JWT auth) is planned but not yet wired into the chart. + +## Storage backend + +- `storage.analytics=Postgres` (default): all analytics paths run through Postgres. The TimescaleDB extensions provide the columnar performance HoldFast needs. **Recommended for most operators.** +- `storage.analytics=ClickHouse`: HoldFast also supports an OTeL-shaped ClickHouse backend, but **this chart does not yet manage the ClickHouse pod**. Bring your own ClickHouse and configure connection via `backend.extraEnv`. + +## Bring-your-own Postgres + +Set `postgres.enabled=false` and configure `externalPostgres.*`: + +```yaml +postgres: + enabled: false + +externalPostgres: + host: my-pg.example.com + port: 5432 + user: holdfast + passwordSecret: + name: my-existing-secret + key: password +``` + +## Lab cluster note + +The `values.lab.yaml` file in this directory is **specific to the BrewingCoder microk8s QA cluster**. Operators self-hosting elsewhere should write their own values file (or set on the command line); `values.lab.yaml` is preserved in-tree only because it serves as a working example of overrides. + +## Development + +```bash +# Render templates without applying (handy for diff'ing against running state): +helm template holdfast . -f values.yaml --set postgres.auth.password=test + +# Lint the chart before committing: +helm lint . + +# Package for OCI registry distribution: +helm package . -d ../../artifacts/helm +``` + +## License + +[AGPL-3.0](https://github.com/BrewingCoder/holdfast/blob/main/LICENSE). diff --git a/infra/helm/holdfast/templates/NOTES.txt b/infra/helm/holdfast/templates/NOTES.txt new file mode 100644 index 00000000..99d2557b --- /dev/null +++ b/infra/helm/holdfast/templates/NOTES.txt @@ -0,0 +1,38 @@ +HoldFast {{ .Chart.AppVersion }} installed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}". + +1. Wait for the backend to become ready: + + kubectl --namespace {{ .Release.Namespace }} \ + rollout status deployment/{{ include "holdfast.fullname" . }}-backend + +2. Reach the dashboard: + +{{- if .Values.publicUrl }} + {{ .Values.publicUrl }} +{{- else }} + (publicUrl was not set; expose the Service named "{{ include "holdfast.fullname" . }}-backend" + on port {{ .Values.backend.service.port }} through your ingress / tunnel of choice) +{{- end }} + +3. Pre-deploy parity check: + + helm --namespace {{ .Release.Namespace }} template {{ .Release.Name }} . \ + -f values.yaml [-f values.lab.yaml] [--set image.tag=] + +Auth mode: {{ .Values.auth.mode }} + {{- if eq .Values.auth.mode "dev" }} + WARNING: dev mode has NO in-app authentication. Front the deployment with + a zero-trust proxy (Cloudflare Access, Authelia, oauth2-proxy) before + exposing to anything beyond a trusted network. + {{- end }} + +Storage backend: {{ .Values.storage.analytics }} + {{- if eq .Values.storage.analytics "ClickHouse" }} + NOTE: ClickHouse pod is NOT yet managed by this chart. Bring your own + ClickHouse instance and configure connection via extraEnv on the + backend. + {{- end }} + +Docs: + - https://github.com/BrewingCoder/holdfast + - https://github.com/BrewingCoder/holdfast/blob/main/docs/HOLDFAST-NOTES.md diff --git a/infra/helm/holdfast/templates/_helpers.tpl b/infra/helm/holdfast/templates/_helpers.tpl new file mode 100644 index 00000000..0bbe318c --- /dev/null +++ b/infra/helm/holdfast/templates/_helpers.tpl @@ -0,0 +1,155 @@ +{{/* +Common helpers for the HoldFast chart. +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "holdfast.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a fully-qualified app name. Honors fullnameOverride or builds +"-" by default. Truncated to 63 chars for k8s name limits. +*/}} +{{- define "holdfast.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Chart name + version for the helm.sh/chart label. +*/}} +{{- define "holdfast.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels shared by every object in the release. +*/}} +{{- define "holdfast.labels" -}} +helm.sh/chart: {{ include "holdfast.chart" . }} +{{ include "holdfast.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: holdfast +{{- end }} + +{{/* +Selector labels — release-stable, used in selector and matchLabels. +*/}} +{{- define "holdfast.selectorLabels" -}} +app.kubernetes.io/name: {{ include "holdfast.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Component-scoped labels for the backend pod. +*/}} +{{- define "holdfast.backend.labels" -}} +{{ include "holdfast.labels" . }} +app.kubernetes.io/component: backend +{{- end }} + +{{- define "holdfast.backend.selectorLabels" -}} +{{ include "holdfast.selectorLabels" . }} +app.kubernetes.io/component: backend +{{- end }} + +{{/* +Component-scoped labels for the postgres pod. +*/}} +{{- define "holdfast.postgres.labels" -}} +{{ include "holdfast.labels" . }} +app.kubernetes.io/component: postgres +{{- end }} + +{{- define "holdfast.postgres.selectorLabels" -}} +{{ include "holdfast.selectorLabels" . }} +app.kubernetes.io/component: postgres +{{- end }} + +{{/* +ServiceAccount name — honors create=false by letting users specify a +pre-existing SA name. +*/}} +{{- define "holdfast.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "holdfast.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Backend image reference. Defaults the tag to .Chart.AppVersion when empty. +*/}} +{{- define "holdfast.image" -}} +{{- $registry := .Values.image.registry -}} +{{- $repo := .Values.image.repository -}} +{{- $tag := default .Chart.AppVersion .Values.image.tag -}} +{{- if $registry -}} +{{- printf "%s/%s:%s" $registry $repo $tag -}} +{{- else -}} +{{- printf "%s:%s" $repo $tag -}} +{{- end -}} +{{- end }} + +{{/* +Postgres host — chart-managed StatefulSet service name OR external host. +*/}} +{{- define "holdfast.postgres.host" -}} +{{- if .Values.postgres.enabled -}} +{{- printf "%s-postgres" (include "holdfast.fullname" .) -}} +{{- else -}} +{{- .Values.externalPostgres.host -}} +{{- end -}} +{{- end }} + +{{- define "holdfast.postgres.port" -}} +{{- if .Values.postgres.enabled -}} +{{- .Values.postgres.service.port -}} +{{- else -}} +{{- .Values.externalPostgres.port -}} +{{- end -}} +{{- end }} + +{{- define "holdfast.postgres.user" -}} +{{- if .Values.postgres.enabled -}} +{{- .Values.postgres.auth.user -}} +{{- else -}} +{{- .Values.externalPostgres.user -}} +{{- end -}} +{{- end }} + +{{/* +Name of the Secret that holds PSQL_PASSWORD. +*/}} +{{- define "holdfast.postgres.secretName" -}} +{{- if .Values.postgres.enabled -}} +{{- if .Values.postgres.auth.existingSecret -}} +{{- .Values.postgres.auth.existingSecret -}} +{{- else -}} +{{- printf "%s-postgres" (include "holdfast.fullname" .) -}} +{{- end -}} +{{- else -}} +{{- .Values.externalPostgres.passwordSecret.name -}} +{{- end -}} +{{- end }} + +{{- define "holdfast.postgres.secretKey" -}} +{{- if .Values.postgres.enabled -}} +{{- .Values.postgres.auth.passwordKey -}} +{{- else -}} +{{- .Values.externalPostgres.passwordSecret.key -}} +{{- end -}} +{{- end }} diff --git a/infra/helm/holdfast/templates/backend-deployment.yaml b/infra/helm/holdfast/templates/backend-deployment.yaml new file mode 100644 index 00000000..ae53f732 --- /dev/null +++ b/infra/helm/holdfast/templates/backend-deployment.yaml @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "holdfast.fullname" . }}-backend + namespace: {{ .Release.Namespace }} + labels: + {{- include "holdfast.backend.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + {{- include "holdfast.backend.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + # Roll the pod when the backend ConfigMap content changes. Standard + # helm idiom — without this, `helm upgrade` of env-only changes + # silently leaves stale pods serving with the old config. + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.backend.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "holdfast.backend.labels" . | nindent 8 }} + {{- with .Values.backend.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "holdfast.serviceAccountName" . }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: backend + image: {{ include "holdfast.image" . | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.backend.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 8082 + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "holdfast.fullname" . }}-backend + env: + - name: PSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "holdfast.postgres.secretName" . }} + key: {{ include "holdfast.postgres.secretKey" . }} + {{- with .Values.backend.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + livenessProbe: + {{- toYaml .Values.backend.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.backend.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + {{- with .Values.backend.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/infra/helm/holdfast/templates/backend-service.yaml b/infra/helm/holdfast/templates/backend-service.yaml new file mode 100644 index 00000000..bb0edb8e --- /dev/null +++ b/infra/helm/holdfast/templates/backend-service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + # NB: microk8s's Cloudflare tunnel routes traffic to this Service by name. + # If you rename this template's metadata.name, coordinate with the cluster + # operator (the tunnel rule needs to match). + name: {{ include "holdfast.fullname" . }}-backend + namespace: {{ .Release.Namespace }} + labels: + {{- include "holdfast.backend.labels" . | nindent 4 }} + {{- with .Values.backend.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.backend.service.type }} + ports: + - port: {{ .Values.backend.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "holdfast.backend.selectorLabels" . | nindent 4 }} diff --git a/infra/helm/holdfast/templates/configmap.yaml b/infra/helm/holdfast/templates/configmap.yaml new file mode 100644 index 00000000..1999c6c1 --- /dev/null +++ b/infra/helm/holdfast/templates/configmap.yaml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "holdfast.fullname" . }}-backend + namespace: {{ .Release.Namespace }} + labels: + {{- include "holdfast.backend.labels" . | nindent 4 }} +data: + # Postgres connection — read by HoldFast.Shared.Runtime.GoEnvCompat. + # Single-underscore names preserved from the legacy Go env-var contract. + PSQL_HOST: {{ include "holdfast.postgres.host" . | quote }} + PSQL_PORT: {{ include "holdfast.postgres.port" . | quote }} + PSQL_USER: {{ include "holdfast.postgres.user" . | quote }} + + # Storage backend selector — the .NET host reads Configuration["Storage:Analytics"], + # which env-var-binds to STORAGE__ANALYTICS (double underscore is the .NET + # convention for nested config keys). Wrong name → defaultBackend falls back + # to "clickhouse" → ClickHouseMigrationService registers → backend crashes + # trying to connect to localhost:8123. Don't drop the second underscore. + STORAGE__ANALYTICS: {{ .Values.storage.analytics | quote }} + + # Frontend URLs — REACT_APP_FRONTEND_URI is the only one the backend reads + # at runtime (via GoEnvCompat → Frontend:Uri). The graph URI vars are baked + # into the frontend bundle at build time and are kept here only as operator + # documentation of intent; they have no runtime effect on the backend. + REACT_APP_FRONTEND_URI: {{ .Values.publicUrl | quote }} + REACT_APP_PUBLIC_GRAPH_URI: {{ .Values.publicGraphUri | quote }} + REACT_APP_PRIVATE_GRAPH_URI: {{ .Values.privateGraphUri | quote }} + + # Auth mode — backend GoEnvCompat maps REACT_APP_AUTH_MODE → Auth:Mode. + # Use REACT_APP_AUTH_MODE (not AUTH_MODE) so the value actually reaches the + # configuration. + REACT_APP_AUTH_MODE: {{ .Values.auth.mode | quote }} + + # OTLP endpoint — for the backend to export its OWN telemetry to (it hosts + # the OTLP receivers itself for incoming data, that's separate). Gate on + # non-empty so empty-string disables self-tracing entirely; setting it to a + # URL the backend can't reach produces noisy 5xx errors with no value. + # The .NET host reads OTEL_EXPORTER_OTLP_ENDPOINT (OTel SDK convention). + {{- if .Values.collectorOtlpEndpoint }} + OTEL_EXPORTER_OTLP_ENDPOINT: {{ .Values.collectorOtlpEndpoint | quote }} + {{- end }} diff --git a/infra/helm/holdfast/templates/postgres-service.yaml b/infra/helm/holdfast/templates/postgres-service.yaml new file mode 100644 index 00000000..12b7bb8b --- /dev/null +++ b/infra/helm/holdfast/templates/postgres-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.postgres.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "holdfast.fullname" . }}-postgres + namespace: {{ .Release.Namespace }} + labels: + {{- include "holdfast.postgres.labels" . | nindent 4 }} +spec: + type: ClusterIP + ports: + - port: {{ .Values.postgres.service.port }} + targetPort: postgres + protocol: TCP + name: postgres + selector: + {{- include "holdfast.postgres.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/infra/helm/holdfast/templates/postgres-statefulset.yaml b/infra/helm/holdfast/templates/postgres-statefulset.yaml new file mode 100644 index 00000000..36cb3142 --- /dev/null +++ b/infra/helm/holdfast/templates/postgres-statefulset.yaml @@ -0,0 +1,102 @@ +{{- if .Values.postgres.enabled -}} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "holdfast.fullname" . }}-postgres + namespace: {{ .Release.Namespace }} + labels: + {{- include "holdfast.postgres.labels" . | nindent 4 }} +spec: + replicas: 1 + serviceName: {{ include "holdfast.fullname" . }}-postgres + selector: + matchLabels: + {{- include "holdfast.postgres.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "holdfast.postgres.labels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.postgres.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: postgres + image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}" + imagePullPolicy: {{ .Values.postgres.image.pullPolicy }} + {{- with .Values.postgres.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: postgres + containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_USER + value: {{ .Values.postgres.auth.user | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "holdfast.postgres.secretName" . }} + key: {{ include "holdfast.postgres.secretKey" . }} + - name: PGDATA + value: {{ .Values.postgres.dataPath | quote }} + # -h 127.0.0.1 forces a TCP-based check via the loopback. The + # default socket-based check looks at /var/run/postgresql which + # TimescaleDB-HA images don't reliably expose. + livenessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgres.auth.user | quote }}, "-h", "127.0.0.1"] + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + readinessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgres.auth.user | quote }}, "-h", "127.0.0.1"] + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + resources: + {{- toYaml .Values.postgres.resources | nindent 12 }} + {{- if .Values.postgres.persistence.enabled }} + volumeMounts: + - name: data + # TimescaleDB-HA roots its data under /home/postgres/pgdata/; + # PGDATA above selects the actual data subdir. + mountPath: /home/postgres/pgdata + {{- end }} + {{- with .Values.postgres.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.postgres.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.postgres.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.postgres.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + labels: + {{- include "holdfast.postgres.labels" . | nindent 10 }} + spec: + accessModes: + {{- toYaml .Values.postgres.persistence.accessModes | nindent 10 }} + {{- if .Values.postgres.persistence.storageClassName }} + storageClassName: {{ .Values.postgres.persistence.storageClassName | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.postgres.persistence.size | quote }} + {{- end }} +{{- end }} diff --git a/infra/helm/holdfast/templates/secret.yaml b/infra/helm/holdfast/templates/secret.yaml new file mode 100644 index 00000000..ce6daac0 --- /dev/null +++ b/infra/helm/holdfast/templates/secret.yaml @@ -0,0 +1,12 @@ +{{- if and .Values.postgres.enabled (not .Values.postgres.auth.existingSecret) -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "holdfast.fullname" . }}-postgres + namespace: {{ .Release.Namespace }} + labels: + {{- include "holdfast.postgres.labels" . | nindent 4 }} +type: Opaque +stringData: + {{ .Values.postgres.auth.passwordKey }}: {{ required "postgres.auth.password is required when postgres.enabled=true and no existingSecret is set" .Values.postgres.auth.password | quote }} +{{- end }} diff --git a/infra/helm/holdfast/templates/serviceaccount.yaml b/infra/helm/holdfast/templates/serviceaccount.yaml new file mode 100644 index 00000000..c394be1f --- /dev/null +++ b/infra/helm/holdfast/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "holdfast.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "holdfast.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/infra/helm/holdfast/values.lab.yaml b/infra/helm/holdfast/values.lab.yaml new file mode 100644 index 00000000..33318056 --- /dev/null +++ b/infra/helm/holdfast/values.lab.yaml @@ -0,0 +1,36 @@ +# Lab-cluster overrides for the BrewingCoder microk8s QA environment. +# Apply with: helm upgrade --install holdfast infra/helm/holdfast \ +# -n holdfast --create-namespace \ +# -f infra/helm/holdfast/values.lab.yaml \ +# --set image.tag= \ +# --set postgres.auth.password= + +image: + registry: localhost:32000 + repository: holdfast-backend-dotnet + pullPolicy: Always # tags overlap across builds in lab + +postgres: + persistence: + storageClassName: nfs-va-vm + auth: + # microk8s pre-created the holdfast-postgres Secret with key "password"; + # chart's secret.yaml template is gated on `not .existingSecret` so it + # won't try to overwrite. + existingSecret: holdfast-postgres + passwordKey: password + +# Public URLs — Cloudflare tunnel terminates TLS at the edge; the backend +# itself serves plain HTTP on :8080. +publicUrl: https://holdfast.brewingcoder.com +publicGraphUri: https://holdfast.brewingcoder.com/public +privateGraphUri: https://holdfast.brewingcoder.com/private +# Self-export of backend telemetry: disabled in QA. The backend hosts its +# own OTLP receivers for incoming data; self-tracing to the public hostname +# 502s through Cloudflare and adds noise without value. Set to a real endpoint +# in operator deployments that want backend traces shipped elsewhere. +collectorOtlpEndpoint: "" + +# Auth handled at the CF Access edge in the lab; app stays in dev mode. +auth: + mode: dev diff --git a/infra/helm/holdfast/values.yaml b/infra/helm/holdfast/values.yaml new file mode 100644 index 00000000..7e2af521 --- /dev/null +++ b/infra/helm/holdfast/values.yaml @@ -0,0 +1,171 @@ +# Default values for the HoldFast helm chart. +# Operators should override anything marked REQUIRED. Lab-specific defaults +# live in values.lab.yaml; never edit values.yaml for environment-specific +# overrides — author a values file or use --set on the command line. + +# ── Image ------------------------------------------------------------------- +image: + # Registry, repository, and tag for the HoldFast backend image. + # Lab cluster overrides registry to localhost:32000 via values.lab.yaml. + registry: ghcr.io + repository: brewingcoder/holdfast-backend-dotnet + # tag defaults to .Chart.AppVersion if empty + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +nameOverride: "" +fullnameOverride: "" + +# ── ServiceAccount --------------------------------------------------------- +serviceAccount: + create: true + annotations: {} + name: "" + +# ── Backend pod ----------------------------------------------------------- +backend: + replicaCount: 1 + + # Backend Kestrel binds on 8082 (see infra/docker/backend-dotnet.Dockerfile + # `ENV ASPNETCORE_URLS=http://+:8082`). If you change the bind port, update + # this AND the cluster operator's tunnel/ingress rule. + service: + type: ClusterIP + port: 8082 + annotations: {} + + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 2000m + memory: 2Gi + + podAnnotations: {} + podLabels: {} + podSecurityContext: {} + securityContext: {} + + # Backend exposes a single /health endpoint (Program.cs uses + # app.MapHealthChecks("/health") — no live/ready split). Probing nested + # paths like /health/live falls through to the SPA index.html fallback + # which returns 200 → probes "pass" for the wrong reason. Don't change + # the path without re-checking what's actually mapped server-side. + livenessProbe: + httpGet: + path: /health + port: 8082 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /health + port: 8082 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 3 + + # Extra environment for the backend container. Use this for operator- + # specific config not covered by the typed values above. + # Each entry: { name: FOO, value: "bar" } or { name: FOO, valueFrom: ... } + extraEnv: [] + + nodeSelector: {} + tolerations: [] + affinity: {} + +# ── Postgres (chart-managed) ---------------------------------------------- +# Set postgres.enabled = false to bring your own database via +# externalPostgres.* below. +postgres: + enabled: true + + image: + repository: timescale/timescaledb-ha + tag: pg16 + pullPolicy: IfNotPresent + + service: + port: 5432 + + persistence: + enabled: true + size: 20Gi + # Empty storageClassName uses the cluster default. Lab cluster overrides + # to nfs-va-vm via values.lab.yaml. + storageClassName: "" + accessModes: + - ReadWriteOnce + + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 2000m + memory: 4Gi + + # TimescaleDB-HA stores data at /home/postgres/pgdata/data — NOT the + # upstream postgres /var/lib/postgresql/data. Don't change this unless + # you also change the image. + dataPath: /home/postgres/pgdata/data + + auth: + user: postgres + # REQUIRED: set via --set postgres.auth.password=... or values file, + # OR reference an existing Secret via existingSecret/passwordKey. + password: "" + existingSecret: "" + passwordKey: password + + # Default fsGroup matches `postgres` UID in timescale/timescaledb-ha:pg16 + # (UID 1000). PSA-restricted clusters require this to be set; permissive + # clusters don't care, so this costs nothing and saves an operator who + # adopts a stricter cluster profile from one debug cycle. + # Override if you swap the image to one that runs as a different UID. + podSecurityContext: + fsGroup: 1000 + securityContext: {} + nodeSelector: {} + tolerations: [] + affinity: {} + +# ── External Postgres (alternative to chart-managed) ---------------------- +# Only used when postgres.enabled = false. +externalPostgres: + host: "" + port: 5432 + user: "" + passwordSecret: + name: "" + key: password + +# ── Storage backend selection --------------------------------------------- +# Postgres = full analytics path via Postgres (no ClickHouse needed). +# ClickHouse = OTeL-shaped columnar store (requires separate ClickHouse pod; +# this chart does not yet manage one — bring your own). +storage: + analytics: Postgres + +# ── External URLs --------------------------------------------------------- +# REQUIRED — operators must set these to the publicly-reachable URLs of +# their HoldFast deployment. No hardcoded domains per the HoldFast charter. +publicUrl: "" # e.g. https://holdfast.example.com +publicGraphUri: "" # e.g. https://holdfast.example.com/public +privateGraphUri: "" # e.g. https://holdfast.example.com/private +collectorOtlpEndpoint: "" # e.g. https://holdfast.example.com/otel + +# ── Auth ------------------------------------------------------------------ +# v1 of this chart only supports auth.mode=dev (no in-app authentication). +# Operators wanting in-app auth should front the deployment with a +# zero-trust proxy (Cloudflare Access, Authelia, oauth2-proxy, etc.) until +# enterprise mode is wired into the chart (planned). +auth: + mode: dev