The platform-utils companion repository¶
platform-utils is the sibling repository to platform-infra. It is the build home for the small, single-purpose utility container images that the platform runs on the EKS clusters — exporters, reporters, cleanup jobs and reconcilers — and, in the app-repo GitOps model, it is also the home of their Kubernetes manifests (k8s/base + k8s/overlays). platform-infra does not vendor these images or their workloads; instead Flux in platform-infra points back at this repo (as a GitRepository source) and at the images this repo pushes to GHCR.
Two repos, one system
platform-utils(this repo) — Python/Bash/Go source,Dockerfiles,docker-bake.hcl, and the Kustomizek8s/tree. It builds+pushes images toghcr.io/example-org/<name>and holds the raw manifests.platform-infra— the fleet GitOps repo.kubernetes/apps/utilswires each component into Flux: a sharedGitRepository(platform-utils), per-componentImageRepository/ImagePolicy/ImageUpdateAutomation, and FluxKustomizations that render this repo'sk8s/overlays/<env>/<component>paths onto the cluster.
Source: platform-utils · docker-bake.hcl · k8s/
Components at a glance¶
Every row below is a Docker Bake target in docker-bake.hcl; each builds to ghcr.io/example-org/<name> (tags :${VERSION} and :latest). Source lives under src/<name>/.
| Component | Built image (ghcr.io/example-org/…) |
Purpose |
|---|---|---|
| camera-probe-propagator | camera-probe-propagator |
Generates Prometheus ScrapeConfig CRDs from camera_profile (camera monitoring) |
| camera-image-size-report | camera-image-size-report |
Analyses S3 image storage per device ID and reports usage to Slack |
| cluster-capacity-analysis | cluster-capacity-analysis |
Kubernetes cluster capacity planning (per-node, KRR hooks); daily CronJob |
| distance-cache-cleanup | distance-cache-cleanup |
Cleans old rows from the camera_distance_cache table (documented in platform-infra) |
| harddisk-hoover | harddisk-hoover |
EKS node disk cleanup (logs, journal, unused container images) |
| mysql-archival | mysql-archival |
Archives old MySQL data to S3 + DB maintenance (monthly CronJob) |
| capture-exporter | capture-exporter |
Prometheus exporter for per-camera capture capture-event counts from Mongo ingest.captures (no PII) |
| s3-bucket-size | s3-bucket-size |
Calculates S3 bucket size/object counts on a schedule |
| sonic-stragglers-report | sonic-stragglers-report |
Reports cameras still talking to the old SonicWall/CHR IP instead of AWS (migration tracking) |
| watchlist-log-items | watchlist-log-items |
Monitors watchlist (watchlist match) log items in Mongo, alerts Slack when none found |
| mikrotik-wireguard-exporter | mikrotik-wireguard-exporter |
Prometheus exporter for RouterOS WireGuard peer health over read-only SSH |
| router-fleet-resolver | router-fleet-resolver |
Classifies MikroTik routers into authenticated (mktxp) vs blackbox tier and emits their scrape config |
| router-lifetime-reconciler | router-lifetime-reconciler |
Aligns MikroTik IPsec IKE/child-SA lifetimes + DPD on AWS VPN peers with the AWS side (audit-first) |
Source dirs that are NOT build targets
src/client-c-migration and src/s3-sums exist under src/ but have no target in docker-bake.hcl and are not built or published. Treat them as scratch/one-off material, not shipped components.
Which page documents which component¶
- distance-cache-cleanup — documented in platform-infra (not deep-documented here; it is listed above only as a one-line row).
- Each remaining built component has its own page in this section (e.g. capture-exporter, mikrotik-wireguard-exporter, camera-probe-propagator, watchlist-log-items, harddisk-hoover, router-fleet-resolver, router-lifetime-reconciler, sonic-stragglers-report, camera-image-size-report, cluster-capacity-analysis, mysql-archival, s3-bucket-size), covering its language/deps, Dockerfile, deployment manifest, config/secrets, and metrics/output.
Multi-arch build system (Docker Bake)¶
Images are built with docker buildx bake against docker-bake.hcl. Key variables (all overridable from the environment):
| Variable | Default | Meaning |
|---|---|---|
VERSION |
latest |
Image tag (set to the semantic-release version by CI) |
REGISTRY |
ghcr.io |
Target registry |
REPO_NAME |
platform-utils |
Used in OCI image.source/image.repo labels |
PLATFORMS |
linux/amd64 |
Comma-separated build platforms (split(",", PLATFORMS) → multi-arch when set, e.g. linux/amd64,linux/arm64) |
Each target sets context = "src/<name>", dockerfile = "Dockerfile", pushes two tags (:${VERSION} and :latest), stamps OCI labels (image.source, image.version, image.created, image.description), and uses BuildKit registry cache (cache-from type=registry …:buildcache, cache-to type=inline) with output = ["type=image,push=true"].
Two groups are defined, and — as currently written — they contain the identical 13 targets:
group "default"— the target list used by a baredocker buildx bake.group "all"— an explicit "build everything" alias with the same 13 targets.
Build examples
Release & versioning (Release Workflows + semantic-release)¶
Releases run through the Release workflow, which delegates to Release Workflows (example-org/release-workflows@v2.3.0), the org's branch-based release action. Versioning tool and image name are auto-detected: Release Workflows reads image_name from docker-bake.hcl and picks semantic-release-python because pyproject.toml carries a [tool.semantic_release] block.
Environment flow (deployment-model bbd, branch-map {"staging":"staging","main":"prod"}):
- Pull requests → Release Workflows
mode: cibuilds a throwawaypr-<N>image (no version bump). - Push to
staging→rcprerelease (prerelease_token = "rc"), taggedv{version}pertag_format. - Push to
main→ stable prod release; builds+pushes the versioned +latestimages and updatesCHANGELOG.md.
Semantic-release parses Conventional-Commit subjects: feat → minor, fix/perf → patch, and major_on_zero = true. paths-ignore skips releases for pure docs/markdown/mkdocs.yml/LICENSE/.gitignore changes. The SELFHOSTED_GITHUB_RUNNER repo variable (shared with the security workflow) can route the job onto the cluster's self-hosted runner pool; blank keeps it on ubuntu-latest.
Kubernetes manifest layout & how platform-infra's Flux consumes it¶
Manifests live under k8s/ as a Kustomize base + overlays tree:
k8s/
├── base/<component>/ # cronjob.yaml / deployment.yaml, rbac, externalsecret, configmap, secret.enc.yaml …
└── overlays/
├── staging/ # kustomization.yaml + resources.yaml (transformer) + per-component patches
└── prod/ # kustomization.yaml + resources.yaml + per-component patches
The overlay kustomization.yaml files select which components ship per environment (staging currently: distance-cache-cleanup, harddisk-hoover, watchlist-log-items, camera-probe-propagator; prod adds capture-exporter; sonic-stragglers-report and camera-image-size-report are commented out "need to fix"). Overlays also carry a Flux SOPS-decryption patch keyed off the app.kubernetes.io/sops=enabled label.
platform-infra drives all of this from kubernetes/apps/utils:
- Source — a single GitRepository named
platform-utils(branchmain, 5m interval, GitHub App auth) influx-system. - Image discovery — per component, an
ImageRepository(ghcr.io/example-org/<name>,ghcr-credentials) and anImagePolicyfiltering tags^v[0-9]+\.[0-9]+\.[0-9]+$with semver range>=1.0.0. - Auto-bump — an
ImageUpdateAutomationcommits the new image tag back into this repo'sk8s/overlays/<env>/<component>path (Setters strategy), authorplatform-bot,[ci skip]— which is whyCHANGELOG.mdis full ofchore: update <component> image to …commits. - Render — a Flux
Kustomizationper component pointspath: ./k8s/overlays/<env>/<component>at theplatform-utilsGitRepository, withapp.kubernetes.io/sops: "enabled"for SOPS decryption.
Secrets
Manifests carry no plaintext credentials. Secrets are provided either via SOPS (secret.enc.yaml, decrypted by Flux — see .sops.yaml below) or via ExternalSecrets (externalsecret.yaml, backed by AWS Secrets Manager). The k8s/README.md documents four secret-provisioning options (kubectl, SealedSecret, ExternalSecret, SOPS).
CI, pre-commit & SOPS¶
- Security gate (security-gate) — the
Securityworkflow runsexample-org/security-gate@v2.7.0onpull_requestonly. Security Gate is the org's MegaLinter-backed gate that fails on net-new findings in the PR diff, producing the requiredsecurity-gatestatus check. It intentionally has no push trigger (so the Release Workflows release commits don't re-scan) and skips Dependabot PRs. DefectDojo/Dependency-Track are deliberately not wired. - pre-commit —
.pre-commit-config.yamlinstallspre-commit+pre-pushhooks fromexample-org/security-gate@v1:shellcheck,actionlint,hadolint,eslint,kustomize,trivy,trufflehog,semgrep,pip-audit,npm-audit,govulncheck,checkov. - SOPS —
.sops.yamlencrypts onlydata/stringData(encrypted_regex: '^(data|stringData)$') with the age recipientage1exampleexampleexampleexampleexampleexampleexampleexamq3n8h5. Flux decrypts these in-cluster via the SOPS overlay patch.