[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-92246":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":15,"stars30d":16,"stars90d":15,"forks30d":15,"starsTrendScore":15,"compositeScore":17,"rankGlobal":9,"rankLanguage":9,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":21,"topics":22,"createdAt":9,"pushedAt":9,"updatedAt":23,"readmeContent":24,"aiSummary":25,"trendingCount":15,"starSnapshotCount":15,"syncStatus":26,"lastSyncTime":27,"discoverSource":28},92246,"ballast","Tight-Line\u002Fballast","Tight-Line","Automated Kubernetes workload resource tracking and adjustment",null,"Go",57,3,51,5,0,4,42.21,"MIT License",false,"main",true,[],"2026-07-22 04:02:05","\u003Cp align=\"center\">\n  \u003Cimg src=\"docs\u002Fimages\u002Fballast-logo-full.png\" alt=\"Ballast — three ballast tanks trimming a ship to level\" width=\"440\">\n\u003C\u002Fp>\n\n# Ballast\n\n[![Known Vulnerabilities](https:\u002F\u002Fsnyk.io\u002Ftest\u002Fgithub\u002FTight-Line\u002Fballast\u002Fbadge.svg)](https:\u002F\u002Fsnyk.io\u002Ftest\u002Fgithub\u002FTight-Line\u002Fballast)\n[![Quality Gate Status](https:\u002F\u002Fsonarcloud.io\u002Fapi\u002Fproject_badges\u002Fmeasure?project=Tight-Line_ballast&metric=alert_status)](https:\u002F\u002Fsonarcloud.io\u002Fsummary\u002Fnew_code?id=Tight-Line_ballast)\n[![codecov](https:\u002F\u002Fcodecov.io\u002Fgh\u002FTight-Line\u002Fballast\u002Fbranch\u002Fmain\u002Fgraph\u002Fbadge.svg)](https:\u002F\u002Fcodecov.io\u002Fgh\u002FTight-Line\u002Fballast)\n\nBallast is a Kubernetes operator that automatically right-sizes workload resource requests and limits based on real operational history. It is a more active alternative to [Fairwinds Goldilocks](https:\u002F\u002Fgithub.com\u002FFairwindsOps\u002Fgoldilocks): rather than suggesting changes, it applies them — at admission time and on running pods via in-place resize (Kubernetes 1.35+).\n\n## What it does\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"docs\u002Fimages\u002Fcpu-requests-vs-usage.png\" alt=\"A cluster's CPU requests pinned against allocatable capacity, then dropping to track real usage once Ballast takes over\" width=\"820\">\n\u003C\u002Fp>\n\nThis is one cluster's CPU over three days. The purple line is allocatable capacity; the blue line is the sum of pod requests — what the scheduler believes is claimed; the red line is what those pods actually burn.\n\nFor the first two days requests ride right up against the ceiling. On paper the cluster is full and can't schedule another pod, yet real usage runs at less than half the reservation. That gap is pure waste: capacity paid for, reserved, and never used.\n\nPartway through 7\u002F3 the workloads are enrolled in Ballast. Requests drop to track observed usage, freeing roughly half the cluster's schedulable CPU without touching a single running workload. That reclaimed headroom is the point: the same nodes now fit far more work, or you run the same work on fewer nodes.\n\n## How it works\n\nWorkloads opt in with annotations on their pod templates. Ballast observes real CPU, memory, and ephemeral-storage utilization, accumulates a rolling history keyed to a *workload identity tuple* (a set of pod labels you configure), and uses that history to right-size all three resources:\n\n1. **Measure** — collect per-container usage samples into a time-series store (Redis\u002FValkey).\n2. **Apply** — patch resource requests and limits at admission time when a pod is created.\n3. **Resize** — adjust resources on running pods via the Kubernetes in-place resize API (1.35+).\n\n`autoresize` is shorthand for all three: it enables measure, apply, and resize with a single annotation. Nothing is applied or resized until the `WorkloadProfile` meets its readiness threshold — that is normal behavior for any resize-enabled workload.\n\nPod eviction for cluster rebalancing is handled by [Kubernetes Descheduler](https:\u002F\u002Fgithub.com\u002Fkubernetes-sigs\u002Fdescheduler) — see the Annotation Contract section for details.\n\n## Prerequisites\n\n- Kubernetes 1.35+ (required for in-place pod resize; earlier versions support measure and apply but not resize)\n- [metrics-server](https:\u002F\u002Fgithub.com\u002Fkubernetes-sigs\u002Fmetrics-server) installed in the cluster (source for CPU and memory; ephemeral-storage usage comes from the kubelet Summary API and needs no extra component)\n- TLS certificate for the admission webhook (see [Webhook TLS](#webhook-tls) below)\n- A Redis-compatible store (Ballast ships with a bundled Valkey via Helm; an existing Redis or Valkey instance works too). The bundled Valkey persists to a PersistentVolumeClaim on the cluster's default StorageClass by default, so accrued history survives pod reschedules; its memory and storage defaults are small and linked, so scale them together (see the annotated `valkey:` block in `values.yaml`).\n\n## Installation\n\ncert-manager must already be installed in the cluster (Ballast does not install it):\n\n```bash\nhelm repo add ballast https:\u002F\u002Ftight-line.github.io\u002Fballast\nhelm repo update\n\nhelm install ballast ballast\u002Fballast \\\n  --namespace ballast-system \\\n  --create-namespace\n```\n\nThe chart ships with a sensible default for `ballastConfig.identityLabels` (`app.kubernetes.io\u002Fname` + `app.kubernetes.io\u002Fcomponent`). Read the section below before overriding it — the choice has cluster-wide consequences.\n\nUpgrades are `helm upgrade --install` with no extra steps. CRDs are kept in sync automatically: Helm itself never upgrades the `crds\u002F` directory, so the chart runs a pre-install\u002Fpre-upgrade hook Job (`ballastd apply-crds`) that server-side-applies the CRD manifests baked into the operator image. Set `crds.upgradeHook.enabled: false` to opt out if external tooling manages CRDs; you are then responsible for applying `config\u002Fcrd\u002Fbases\u002F` on every upgrade.\n\n## WorkloadProfile Identity\n\nBallast groups pods into `WorkloadProfile` objects by matching a configurable set of pod label keys called the **identity tuple**. The tuple is defined once in `BallastConfig.spec.identityLabels` and applies to every namespace in the cluster.\n\n**WorkloadProfiles are cluster-scoped.** Every pod in every namespace that shares the same label values for the identity keys feeds measurements into the same profile. This is intentional: forty dev namespaces all running the same billing app produce one well-sampled `WorkloadProfile`, not forty thin ones.\n\n### Default: `name` + `component`\n\n```yaml\nballastConfig:\n  identityLabels:\n    - app.kubernetes.io\u002Fname\n    - app.kubernetes.io\u002Fcomponent\n```\n\nThis works well for clusters that run a single environment class. The frontend and backend of the same app get separate profiles; forty developers' copies of billing all contribute to the same `(billing, api)` profile.\n\n### Mixed environments in the same cluster\n\nIf your cluster runs dev, staging, and production side-by-side and you want separate profiles per environment, add `ballast.tightlinesoftware.com\u002Fresource-profile` to the identity tuple and apply it to your pods:\n\n```yaml\n# BallastConfig \u002F Helm values\nballastConfig:\n  identityLabels:\n    - app.kubernetes.io\u002Fname\n    - app.kubernetes.io\u002Fcomponent\n    - ballast.tightlinesoftware.com\u002Fresource-profile\n```\n\n```yaml\n# Pod template labels\nlabels:\n  app.kubernetes.io\u002Fname: billing\n  app.kubernetes.io\u002Fcomponent: api\n  ballast.tightlinesoftware.com\u002Fresource-profile: prod\n```\n\nNow `(billing, api, prod)` and `(billing, api, dev)` are measured independently. Pods without the `ballast.tightlinesoftware.com\u002Fresource-profile` label get a placeholder value in the profile name (`noresourceprofile`) rather than being skipped, so opted-in pods always produce a profile.\n\n> **Changing `identityLabels` wipes your operational history.** It redefines what constitutes a workload identity, so all existing `WorkloadProfile` objects are renamed and their accumulated Redis history is orphaned. Ballast starts fresh from zero samples. Plan your tuple before enrolling workloads.\n\n## Annotation Contract\n\nAdd these annotations to your pod template specs to enroll workloads. Ballast never acts on a workload without explicit opt-in.\n\n| Annotation | Meaning |\n|---|---|\n| `ballast.tightlinesoftware.com\u002Fmeasure: \"true\"` | Collect metrics; required for any other behavior |\n| `ballast.tightlinesoftware.com\u002Fapply: \"true\"` | Patch requests\u002Flimits at admission time; requires `measure` |\n| `ballast.tightlinesoftware.com\u002Fresize: \"true\"` | Adjust resources on running pods via in-place resize; requires `apply` |\n| `ballast.tightlinesoftware.com\u002Fautoresize: \"true\"` | Shorthand for `measure` + `apply` + `resize`; mutually exclusive with `apply` and `resize` |\n\n**`apply` and `resize` do not cover the same resources.** At admission time the webhook can set *any* recommended resource — cpu, memory, ephemeral-storage — because it patches the pod spec before the pod exists. In-place resize is narrower by Kubernetes design: the pod resize subresource ([KEP-1287](https:\u002F\u002Fgithub.com\u002Fkubernetes\u002Fenhancements\u002Ftree\u002Fmaster\u002Fkeps\u002Fsig-node\u002F1287-in-place-update-pod-resources)) permits mutating only `cpu` and `memory` on a running pod, and rejects a patch that touches anything else. Ballast therefore excludes all other resources from resize patches. In practice this means an `ephemeral-storage` recommendation takes effect only when a pod is recreated (via `apply`), never in place; a running pod whose ephemeral-storage drifts will keep its current value until its next restart. Ballast logs every exclusion; when non-resizable drift is the *only* drift on a pod (so no resize is issued at all), it also records `ballast.resize.skipped{reason=\"not_resizable\"}` — skip reasons always describe the whole pod, never a single resource axis.\n\n**`resize` cannot change a pod's QoS class.** A pod's QoS class (`BestEffort`, `Burstable`, `Guaranteed`) is fixed at creation, and the resize subresource rejects any patch that would change it. Two recommendation shapes run into this: a `BestEffort` pod (no requests or limits on any container) can never gain requests in place, and a `Guaranteed` pod (requests equal to limits for cpu and memory everywhere) can only be resized by moving requests and limits together. Ballast detects both before patching and records `ballast.resize.skipped{reason=\"qos_pinned\"}` instead of attempting a resize that cannot succeed; the recommendation still applies at admission time (via `apply`) when the pod is next recreated. When a resize fails for a reason Ballast could not predict, the pod is annotated `ballast.tightlinesoftware.com\u002Fresize-blocked` with the error text plus `resize-blocked-at` with the failure time, and further attempts are skipped (`reason=\"blocked\"`) until one resize interval has elapsed; a later successful resize clears both annotations.\n\n**Pod eviction** is deliberately out of scope for Ballast. Ballast keeps resource requests and limits accurate; cluster rebalancing based on those corrected values is best handled by [Kubernetes Descheduler](https:\u002F\u002Fgithub.com\u002Fkubernetes-sigs\u002Fdescheduler) (specifically its `LowNodeUtilization` strategy). This is a clean division of labor: Ballast gets the weight right, Descheduler decides where pods should sit.\n\n**Example — full automation:**\n\n```yaml\nspec:\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io\u002Fname: billing\n        ballast.tightlinesoftware.com\u002Fresource-profile: prod\n      annotations:\n        ballast.tightlinesoftware.com\u002Fautoresize: \"true\"\n```\n\n**Example — measure only (safe first step):**\n\n```yaml\nspec:\n  template:\n    metadata:\n      annotations:\n        ballast.tightlinesoftware.com\u002Fmeasure: \"true\"\n```\n\n### Which workloads to enroll\n\nBallast right-sizes **long-running** workloads by learning their steady-state usage over hours or days (default readiness: 250 samples over 24 hours). Enroll Deployments, StatefulSets, DaemonSets, and similar controllers whose pods run continuously.\n\n- **Job pods: you almost certainly do not want to annotate them.** A Job runs to completion, often in seconds or minutes, so it never accumulates enough steady-state history to cross the readiness threshold. Annotating a Job's pod template only creates a `WorkloadProfile` that never produces a recommendation. Ballast will not stop you (opt-in is entirely under your control), but there is nothing to gain and it clutters your profiles.\n- **CronJob pods: think hard before annotating them.** A CronJob creates Jobs, and those Jobs create the pods (`CronJob → Job → Pod`), so CronJob pods carry the same run-to-completion caveat as any other Job pod. Annotate them only if each run is genuinely long-lived and resource-stable enough to measure meaningfully — for example, a multi-hour nightly batch job. A short or spiky periodic task is a poor fit and will mostly generate noise.\n\n**Only regular containers are right-sized today.** Even on an enrolled pod, Ballast measures and resizes only the pod's regular `spec.containers`. It excludes **all init containers and ephemeral debug containers** from measurement — there is no per-container knob to configure; the exclusion is automatic. Note this is broader than ideal: restartable-init \"native sidecar\" containers (`restartPolicy: Always`) are long-running and would be good right-sizing targets, but they are excluded for now because the apply and resize paths only patch `spec.containers`. Support for right-sizing restartable-init sidecars is tracked in [#30](https:\u002F\u002Fgithub.com\u002FTight-Line\u002Fballast\u002Fissues\u002F30).\n\n## Verifying a WorkloadProfile\n\nOnce a pod with the `measure` annotation is running, Ballast creates a `WorkloadProfile` for its identity tuple. Check it with:\n\n```bash\nkubectl get workloadprofiles\nkubectl describe workloadprofile billing--api--prod\n```\n\nThe profile status shows accumulated usage statistics and recommendations once the readiness threshold is met (default: 250 samples collected over 24 hours). CPU, memory, and ephemeral storage are all tracked and sized:\n\n```yaml\nstatus:\n  containers:\n    - name: app\n      usageStats:\n        - resource: cpu\n          samples: 288\n          mean: \"230m\"\n          p95: \"240m\"\n          p99: \"310m\"\n          cv: \"0.46\"\n        - resource: memory\n          samples: 288\n          mean: \"180Mi\"\n          p50: \"176Mi\"\n          p75: \"192Mi\"\n          p95: \"210Mi\"\n          p99: \"240Mi\"\n          cv: \"0.21\"\n        - resource: ephemeral-storage\n          samples: 288\n          p90: \"1200Mi\"\n          p99: \"1800Mi\"\n          cv: \"0.33\"\n      recommendations:\n        cpu:\n          request: \"288m\"     # avg * 1.25 headroom\n        memory:\n          request: \"192Mi\"    # p75\n          limit: \"288Mi\"      # p99 * 1.2\n        ephemeral-storage:\n          request: \"1200Mi\"   # p90\n          limit: \"2160Mi\"     # p99 * 1.2\n  meetsThreshold: true\n  activeWorkloads: 3\n```\n\n## Kill Switch\n\nCreate a ConfigMap named `ballast-kill-switch` in the operator namespace to immediately halt all Ballast activity without a restart:\n\n```bash\n# Halt all Ballast activity\nkubectl create configmap ballast-kill-switch -n ballast-system\n\n# Resume\nkubectl delete configmap ballast-kill-switch -n ballast-system\n```\n\nAll suppressed actions are logged at `warn` level with `kill_switch: true`. Pod admission continues normally (webhook passes pods through without mutation).\n\nFor planned, GitOps-managed suspension, set `BallastConfig.spec.suspended: true` instead.\n\n## Webhook TLS\n\nKubernetes requires the admission webhook server to present a TLS certificate trusted by the API server. Ballast supports three approaches, in order of preference:\n\n**1. cert-manager (default)**\n\nThe Helm chart creates a self-signed `Issuer` and a `Certificate` resource. cert-manager provisions the cert, mounts it into the operator pod, and injects the CA bundle into the `MutatingWebhookConfiguration` automatically. This works on air-gapped clusters — no DNS or HTTP challenge, no external CA.\n\nRequires [cert-manager](https:\u002F\u002Fcert-manager.io) already installed in the cluster (Ballast uses it but does not install it). If cert-manager is already present — a common case — no extra setup is needed.\n\n```yaml\n# values.yaml (default)\ncertManager:\n  enabled: true\n```\n\n**2. Kubernetes CertificateSigningRequest (future improvement)**\n\nA Helm pre-install Job submits a CSR to the cluster's built-in CA. The resulting cert is written to a Secret that the operator mounts. No cert-manager dependency, but requires the Job's ServiceAccount to have `certificates.k8s.io\u002Fapprove` permission — which some clusters restrict, requiring manual `kubectl certificate approve`.\n\nNot yet implemented; tracked as a future Helm chart improvement.\n\n**3. User-provided certificate (future improvement)**\n\nSupply your own cert material (e.g. from an internal PKI or Vault) via Helm values. The chart skips cert-manager and CSR resources entirely and uses the provided Secret directly. The `caBundle` in the `MutatingWebhookConfiguration` must be set to the corresponding CA cert.\n\nNot yet implemented; tracked as a future Helm chart improvement.\n\n## Default MetricsSource and ClusterResourcePolicy\n\nA fresh `helm install` ships three objects out of the box so measurements work without any extra setup: two `MetricsSource` objects (CPU\u002Fmemory and ephemeral storage) and one catch-all `ClusterResourcePolicy`.\n\n### MetricsSource: `kubernetes-metrics`\n\n```yaml\nspec:\n  type: kubernetesMetrics\n  config:\n    pollInterval: \"300s\"\n    reservoirSize: 10000\n```\n\nThis wires Ballast to the cluster's [metrics-server](https:\u002F\u002Fgithub.com\u002Fkubernetes-sigs\u002Fmetrics-server) (which must already be installed — it is not bundled) for CPU and memory. Samples are collected every 5 minutes and up to 10,000 samples per container per metric are retained in Redis.\n\n### MetricsSource: `kubelet-summary`\n\n```yaml\nspec:\n  type: kubeletSummary\n  config:\n    pollInterval: \"300s\"\n    reservoirSize: 10000\n```\n\nThis reads ephemeral-storage usage from the kubelet Summary API (via the API server proxy). No extra credentials are needed beyond the Ballast ServiceAccount.\n\nTo opt out of either source and manage `MetricsSource` objects yourself, set `enabled: false` on the relevant entry:\n\n```yaml\n# values.yaml\ndefaultMetricsSources:\n  kubernetesMetrics:\n    enabled: false\n  kubeletSummary:\n    enabled: false\n```\n\n### ClusterResourcePolicy: `default`\n\nThis is the `homogeneous-large-fleet` preset, the chart's built-in default:\n\n```yaml\nspec:\n  priority: 0\n  metrics:\n    - resource: cpu\n      field: request\n      source: kubernetes-metrics\n      aggregation: avg\n      headroom: \"1.25\"\n    - resource: memory\n      field: request\n      source: kubernetes-metrics\n      aggregation: p50\n      headroom: \"1.05\"\n    - resource: memory\n      field: limit\n      source: kubernetes-metrics\n      aggregation: p99\n      headroom: \"1.2\"\n    - resource: ephemeral-storage\n      field: request\n      source: kubelet-summary\n      aggregation: p90\n      headroom: \"1.0\"\n    - resource: ephemeral-storage\n      field: limit\n      source: kubelet-summary\n      aggregation: p99\n      headroom: \"1.2\"\n  readiness:\n    minDataPoints: 250\n    minTimeSpan: \"24h\"\n    maxCV: \"1.5\"\n    cvMeanFloor:\n      cpu: \"25m\"\n      memory: \"25Mi\"\n      ephemeral-storage: \"2Mi\"\n  behaviors:\n    thresholds:\n      default: \"10%\"\n    resize:\n      maxChangePerCycle: \"50%\"\n      interval: \"15m\"\n```\n\nThis catch-all policy applies to every opted-in pod in the cluster. Key design decisions:\n\n- **CPU request at `avg * 1.25`.** CPU usage is spiky and idles far below its bursts, so sizing the request at 80% of mean (= mean \u002F 0.80 target utilization) keeps nodes dense while leaving headroom for normal variation. For a large homogeneous fleet the aggregate pressure is predictable, so the mean is a reliable basis.\n- **Memory request at `p75`, deliberately not the CPU formula.** Memory working set is occupancy, not utilization: it is nearly flat per container (p99 typically sits within ~15-30% of the mean), so a utilization-target formula like `avg * 1.25` would put every request above the container's all-time observed p99 and pin the fleet's aggregate reservation at 125% of aggregate usage, all of it reserved-but-unusable capacity. Spike protection is the limit's job, not the request's; the request only needs to state typical occupancy so the scheduler's claim matches reality. p75 states that occupancy directly: the request covers actual usage ~75% of the time, so the scheduler's claim sits at or just above real usage rather than under it, and it self-adjusts to each workload's spread (a flat container's p75 is within a few percent of its p50, while a variable one gets proportionally more room). p75 stays clear of the tail where brief startup spikes live, so it remains stable enough not to re-trip the drift threshold.\n- **Memory limit at `p99 * 1.2`.** p99 is the highest usage the workload has shown in production; the 20% headroom absorbs a normal rare spike while still OOMKilling a pod that runs well past its observed peak (a likely leak). This yields Burstable QoS (limit > request), the right class for most production workloads. **CPU limits are intentionally omitted** — they cause throttling rather than reclaiming waste.\n- **Ephemeral storage from the kubelet Summary API.** The request is sized at p90 (the growth-skewed distribution) and the limit at `p99 * 1.2` so the kubelet evicts a genuine runaway pod before the node hits disk pressure while tolerating a normal spike above the observed peak.\n- **250 samples over 24 hours before acting.** At the 5-minute poll interval a single long-running pod accrues ~288 samples in 24h, so the 24h window — not the sample count — is the binding constraint. A high coefficient of variation (CV > 1.5) also blocks action — it means the workload is too spiky to size reliably. The CV check is skipped when mean usage sits below a tiny per-resource floor (`cvMeanFloor`, defaults: 25m CPU, 25Mi memory, 2Mi ephemeral-storage): CV divides by the mean, so near-idle workloads produce huge CVs from quantization noise and rare startup spikes alone, and without the floor a single near-idle resource would pin the whole profile at `Accruing` forever — blocking recommendations for every other resource. Usage below the floor is too small for a mis-sized recommendation to matter.\n- **10% drift threshold.** A resize only fires when the current resource value deviates from the recommendation by more than 10%. In-place resize is cheap and safe (a request\u002Flimit patch on a running pod, no restart), so the band is deliberately tight: a recommendation that has moved more than 10% reflects a real shift in observed usage worth acting on, not noise.\n- **50% max change per cycle.** Each resize moves at most half the remaining gap between the current value and the recommendation, giving workloads time to stabilize between adjustments. The first step makes most of the correction; once a step would land within the drift threshold, the recommendation is applied exactly, so convergence completes instead of stalling just inside the threshold.\n- **Priority 0.** This is the lowest possible priority. Any `ClusterResourcePolicy` or `ResourcePolicy` with `priority > 0` wins for matched workloads, so you can override specific namespaces or workload kinds without touching this default.\n\n### Policy presets\n\nThe default above is one entry in a catalog of presets — Helm values overlays under [`charts\u002Fballast\u002Fpresets\u002F`](charts\u002Fballast\u002Fpresets\u002FREADME.md) that retune the policy for a particular operating profile. `homogeneous-large-fleet` is built into `values.yaml`; `local-testing` is a fast-cycle overlay for kind clusters. Select one at install time with `-f`:\n\n```bash\nhelm install ballast ballast\u002Fballast -n ballast-system --create-namespace \\\n  -f charts\u002Fballast\u002Fpresets\u002Flocal-testing.yaml\n```\n\nA later `-f` file or `--set` deep-merges on top (map fields merge; list fields like `metrics` are replaced wholesale), so you can layer a preset and override a single field.\n\nTo opt out and manage policies yourself:\n\n```yaml\n# values.yaml\ndefaultClusterResourcePolicy:\n  enabled: false\n```\n\nTo override just the readiness threshold or headroom for all workloads, patch the values directly:\n\n```yaml\n# values.yaml\ndefaultClusterResourcePolicy:\n  readiness:\n    minDataPoints: 200\n    minTimeSpan: \"6h\"\n  metrics:\n    - resource: cpu\n      field: request\n      aggregation: avg\n      headroom: \"1.2\"\n    - resource: memory\n      field: request\n      aggregation: avg\n      headroom: \"1.2\"\n```\n\n> **Note:** `metrics` is a list, so overriding it replaces the entire default list (including the memory-limit and ephemeral-storage entries). Repeat every entry you want to keep.\n\nTo add a tighter policy for production namespaces alongside the default, create a higher-priority `ClusterResourcePolicy`:\n\n```yaml\napiVersion: ballast.tightlinesoftware.com\u002Fv1\nkind: ClusterResourcePolicy\nmetadata:\n  name: production\nspec:\n  priority: 10\n  selector:\n    namespaces:\n      include: [\"\u002F.*-prod\u002F\", \"\u002F.*-production\u002F\"]\n  metrics:\n    - resource: cpu\n      field: request\n      source: kubernetes-metrics\n      aggregation: p99\n      headroom: \"1.15\"\n    - resource: memory\n      field: request\n      source: kubernetes-metrics\n      aggregation: p99\n      headroom: \"1.25\"\n  readiness:\n    minDataPoints: 1000\n    minTimeSpan: \"72h\"\n    maxCV: \"1.0\"\n```\n\nState only your intentional deviations: any `readiness` or `behaviors` field you omit (here, `cvMeanFloor` and all of `behaviors`) is filled with the documented default by the operator **when the policy is resolved**, so sparse policies automatically track the current release's defaults across upgrades. Nothing is baked into the stored object at write time. `kubectl get` shows only what you wrote; `kubectl explain clusterresourcepolicy.spec.readiness` documents the effective defaults. To disable the `cvMeanFloor` exemption entirely, set it to an explicit empty map (`cvMeanFloor: {}`).\n\n## Dry-run Mode\n\nEach action has an independent dry-run flag. They cascade: dry-running `measure` implies dry-running everything downstream.\n\n| Flag | Helm value | Effect |\n|---|---|---|\n| `--dry-run-measure` | `dryRun.measure` | Compute stats, log what would be written; no Redis writes |\n| `--dry-run-apply` | `dryRun.apply` | Log the patch; pod admitted without modification |\n| `--dry-run-resize` | `dryRun.resize` | Log the resize; pod not touched |\n\nAll dry-run actions are logged at `info` level with `dry_run: true`.\n\n## Development\n\n```bash\n# Prerequisites\nmake tools          # Install goimports\nmake setup-hooks    # Install pre-commit hook\n\n# Common workflow\nmake check          # Full gate: lint + 100% coverage + build\nmake build          # Build bin\u002Fballastd\nmake test           # Run tests with envtest\nmake lint-fix       # Auto-fix lint issues\nmake fmt            # Format code\n\n# CRD \u002F code generation (run after editing api\u002F*_types.go)\nmake manifests      # Regenerate CRDs, RBAC, and webhook manifests\nmake generate       # Regenerate DeepCopy methods\n```\n\nThe `make check` target is the gate for every PR and release. It runs golangci-lint, enforces 100% test coverage (uncovered lines require a `\u002F\u002F coverage:ignore - \u003Creason>` comment), and builds the binary.\n\n### Local kind cluster\n\nFor iterating against a real cluster without pushing to GHCR, use the `helm-update-local` workflow. It builds a local image, loads it directly into kind (no registry push\u002Fpull), and installs the Helm chart.\n\n**One-time setup**\n\n```bash\n# Create the kind cluster (any name works; pass it to every make command below)\nkind create cluster --name ballast-dev\n\n# Install cert-manager (required by the Ballast webhook)\nhelm repo add jetstack https:\u002F\u002Fcharts.jetstack.io --force-update\nhelm install cert-manager jetstack\u002Fcert-manager \\\n  --namespace cert-manager --create-namespace \\\n  --set crds.enabled=true\n\n# Wait for cert-manager to be ready before deploying Ballast\nkubectl rollout status deployment\u002Fcert-manager -n cert-manager\n\n# Install metrics-server (required for the kubernetesMetrics plugin)\n# kind nodes don't have valid kubelet certs, so --kubelet-insecure-tls is required\nkubectl apply -f https:\u002F\u002Fgithub.com\u002Fkubernetes-sigs\u002Fmetrics-server\u002Freleases\u002Flatest\u002Fdownload\u002Fcomponents.yaml\nkubectl patch deployment metrics-server -n kube-system \\\n  --type=json \\\n  -p='[{\"op\":\"add\",\"path\":\"\u002Fspec\u002Ftemplate\u002Fspec\u002Fcontainers\u002F0\u002Fargs\u002F-\",\"value\":\"--kubelet-insecure-tls\"}]'\nkubectl rollout status deployment\u002Fmetrics-server -n kube-system\n```\n\n**Iterate: change code → rebuild → redeploy**\n\n```bash\nmake helm-update-local KIND_CLUSTER=ballast-dev\n```\n\nThis runs three steps in sequence:\n\n1. **Build** — `docker build --platform linux\u002F\u003Chost-arch>` tagged `:local`. The host architecture is detected automatically via `uname -m`, so the same command works on both ARM and x86 machines.\n2. **Load** — `kind load docker-image` injects the image directly into the kind node; no registry push or GHCR credentials needed.\n3. **Install** — `helm upgrade --install` deploys the chart into `ballast-system` with `image.pullPolicy=Never`, pinning it to the locally loaded image, and applies the `local-testing` policy preset (`-f charts\u002Fballast\u002Fpresets\u002Flocal-testing.yaml`) for fast feedback.\n\n**Individual targets** (when you only need part of the cycle):\n\n```bash\nmake docker-kind KIND_CLUSTER=ballast-dev      # Build + load image only\nmake helm-install-local                        # Install\u002Fupgrade chart only (uses last loaded image)\n```\n\n**Verify the deployment**\n\n```bash\nkubectl get pods -n ballast-system             # operator pod should be Running\nkubectl logs -n ballast-system -l app.kubernetes.io\u002Fname=ballast -f\nkubectl get ballastconfig                      # confirm CRD is installed\n```\n\nFor the full measure → apply → resize walkthrough on a local cluster, see [TESTING.md](TESTING.md).\n\n## Logging\n\nPer-component log levels are configurable at startup:\n\n```bash\nballastd \\\n  --log-level=info \\\n  --log-level-webhook=debug \\\n  --log-level-collector=warn \\\n  --log-format=text    # json (default) or text\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## License\n\nCopyright 2026 Tight Line Software LLC.\n\nLicensed under the MIT License. See [LICENSE](LICENSE) for the full text.\n","Ballast 是一个 Kubernetes 运算符，用于自动跟踪和动态调整工作负载的 CPU、内存及临时存储资源请求与限制。它基于真实运行时指标（如历史利用率）持续优化资源配置，支持准入时注入和运行中就地调整（需 Kubernetes 1.35+），并依赖 Redis\u002FValkey 存储时间序列数据。相比仅提供建议的工具（如 Goldilocks），Ballast 直接执行资源重设，显著提升集群资源利用率。适用于需要精细化资源治理、降低云成本、支撑多租户或高密度调度的生产 Kubernetes 环境。",2,"2026-07-08 04:29:57","CREATED_QUERY"]