gantry

package
v0.22.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: Apache-2.0 Imports: 2 Imported by: 0

README

Gantry deployment artifacts

This directory carries the Helm chart and operator-facing rendered manifests needed to roll out the Gantry agent as a Kubernetes DaemonSet.

Files

The Helm chart under chart/ is the source of truth for resources shared by standalone and operator-managed installations. make gantry-manifests renders the internal operator profile into deploy/gantry/rendered/; the Unbounded operator embeds those files and applies its own image, ConfigMap, and Lease ownership semantics. The target also renders the standalone node configurator and examples used by development and benchmark tooling.

Source Rendered to Purpose
chart/templates/daemonset.yaml rendered/daemonset.yaml One-pod-per-node DaemonSet.
chart/templates/serviceaccount.yaml rendered/serviceaccount.yaml Namespace + ServiceAccount + Role + PriorityClass.
chart/templates/configmap.yaml rendered/configmap.yaml Default config.yaml (mirrors config.NewDefault()).
chart/templates/rendezvous-leases.yaml rendered/rendezvous-leases.yaml Fixed chair Lease set.
chart/templates/node-config.yaml Standalone chart only Continuously reconciles containerd's default Gantry mirror route.
examples/registry-secret.example.yaml.tmpl rendered/examples/registry-secret.example.yaml Template Secret for upstream-registry credentials.
examples/networkpolicy.yaml.tmpl rendered/examples/networkpolicy.yaml Hardening overlay (NOT applied by default). See Hardening overlays below.
hosts.toml.template (not rendered) containerd registry mirror config; one file per upstream registry under /etc/containerd/certs.d/<host>/hosts.toml.

Installation paths

  • Operator-managed clusters use the manifests embedded in the unbounded-operator binary. The operator never runs Helm.
  • Clusters without the operator install the released OCI chart. The chart continuously reconciles /etc/containerd/certs.d/_default/hosts.toml on every selected node. Containerd must already be configured to read /etc/containerd/certs.d; the chart does not edit or restart containerd.

The paths are mutually exclusive. PriorityClass/gantry-low records the active manager, and both installers reject ownership by the other path.

helm upgrade --install gantry oci://ghcr.io/azure/charts/gantry \
   --version <release-without-v> \
   --namespace gantry-system \
   --create-namespace \
   --set image.digest=sha256:<gantry-image-digest> \
   --set gantry.upstreamRegistries[0].name=registry.example.com \
   --set gantry.upstreamRegistries[0].endpoint=https://registry.example.com

The container image is built from images/gantry/Containerfile via make image-gantry-local (or make image-gantry-push to push).

Chair sizing and upgrades

gantry.chairCount controls the maximum active chair pool and gantry.chairSeedCount controls the per-digest seed cohort at a full pool. Both default to 64 chairs and 8 seeds. The counts have no configured maximum, but both must be positive and the seed count cannot exceed the chair count. The active pool is also bounded by the Gantry DaemonSet's desired scheduled pod count because one agent can hold at most one chair. The previous chair_holder_count, GANTRY_CHAIR_HOLDER_COUNT, and --chair-holder-count names remain accepted as deprecated aliases.

For example:

helm upgrade gantry oci://ghcr.io/azure/charts/gantry \
   --version <release-without-v> \
   --namespace gantry-system \
   --reuse-values \
   --set gantry.chairCount=128 \
   --set gantry.chairSeedCount=100

Increasing the chair count preserves every existing Lease and holder. Agents create and claim the additional numbered Leases through the normal election path until the new target is reached. Decreasing it makes holders at IDs above the new count vacate; the empty Lease objects may remain and are ignored. Chair Leases carry Helm's keep resource policy so a reduction does not delete coordination state during a rolling update. This also leaves the Leases behind on uninstall; delete them explicitly only after every Gantry pod has stopped.

Releases that only understand the original 64-chair set reject higher-numbered Lease names. Upgrading from such a release therefore requires two stages:

  1. Upgrade every Gantry pod to the count-aware image while keeping gantry.chairCount=64.
  2. After that rollout completes, raise gantry.chairCount and gantry.chairSeedCount.

Do not combine these stages in one Helm upgrade. A new pod can create a higher-numbered Lease while old pods are still running, which makes the old pods reject their next chair snapshot.

Metrics

Each agent serves Prometheus metrics on port 9095 (/metrics). Set metricsService.enabled=true to render Service/gantry-metrics, a headless Service over the agents that also lists NotReady agents. Use metricsService.annotations for annotation-driven scrapers, for example a Datadog endpoints check that scrapes every agent from its own node:

metricsService:
  enabled: true
  annotations:
    ad.datadoghq.com/endpoints.checks: |
      {"openmetrics": {"init_config": {}, "instances": [{
        "openmetrics_endpoint": "http://%%host%%:9095/metrics",
        "namespace": "gantry", "metrics": [".*"]}]}}

## Agent rollout

`updateStrategy.rollingUpdate.maxUnavailable` sets the agent DaemonSet's
rolling-update step as a positive count or percentage. The default `50%`
replaces half of the agents at once; lower it on large clusters to bound how
many nodes lose their local mirror during a rollout:

```sh
--set-string updateStrategy.rollingUpdate.maxUnavailable=10%

Peer transfer tuning

gantry.peerFetchTimeout (default 15m) caps one complete peer blob request, including streaming the body. It is a safety ceiling, not a stall detector: the HTTP/2 transport separately probes connections after 10s without reads. A value much lower than a large layer's transfer time makes requesters abandon progressing transfers and switch providers.

gantry.transferMaxConcurrentServes (default 10) caps concurrent peer blob serves per agent. Requests over the cap receive 429 so the requester re-discovers another provider. 0 means unlimited.

Use gantry.config only when a setting has no dedicated value; it replaces the entire generated configuration.

Operator Profile

# Render the profile embedded by unbounded-operator.
make gantry-manifests

# Validate and package the standalone chart.
make gantry-chart-lint
make gantry-chart-package GANTRY_CHART_VERSION=0.1.0 GANTRY_CHART_APP_VERSION=v0.1.0

Building the image locally

# Single-arch into local container engine:
make image-gantry-local

# Build and push to $(CONTAINER_REGISTRY):
make image-gantry-push CONTAINER_REGISTRY=ghcr.io/your-org

# Explicit tag:
make image-gantry-local VERSION=v0.6.0

Per-node containerd setup

Nodes provisioned by unbounded-agent already have containerd configured to read /etc/containerd/certs.d and carry the managed default Gantry mirror entry in /etc/containerd/certs.d/_default/hosts.toml. On those nodes, install the Gantry DaemonSet normally; the mirror activates when the pod starts listening on 127.0.0.1:5000.

ACR attachment is separate from deployment and preserves existing authentication by default. For operator-managed AKS Flex clusters, see the ACR attachment guide for commands, managed-auth opt-in, compatible versions, and limitations.

Standalone Helm installations run DaemonSet/gantry-containerd-config. Its resident reconciler checks the default hosts.toml every five seconds and atomically restores the chart-owned payload when the file is missing or different, including after a node upgrade resets host configuration. Graceful shutdown removes the file only when it still matches the chart payload. Set nodeConfig.enabled=false when another node-management system owns this file.

Externally managed installations can instead drop a registry-specific hosts.toml at:

/etc/containerd/certs.d/<registry-host>/hosts.toml

derived from hosts.toml.template (substitute ${REGISTRY_SERVER} with the registry's https://... URL). containerd reloads certs.d on its own; no restart needed.

What to verify after rollout

Check How
Agents are running kubectl -n unbounded-system get ds gantry
Liveness / readiness /livez, /readyz on 9095 per pod
Metrics curl http://<pod-ip>:9095/metrics or scrape from Prometheus
Routing-table grew p2p_dht_health_score ≥ 0.7
Mirror is being used p2p_cache_hit_total increments while a workload rolls out (= containerd content-store hits on the mirror endpoint after Phase 8)
Storage backend is containerd gantry_storage_mode_info{mode="containerd"} == 1
Advertiser reconciling gantry_advertise_reconcile_total increases at the configured cadence
Leases are being created on please_pull gantry_containerd_lease_created_total increments during cold-start rollouts
Origin fallback is rare p2p_origin_fallback_total stays at ~0

See docs/detailed-design.md §7.6 for the full metric catalog.

Hardening overlays

deploy/examples/ carries optional hardening manifests that are intentionally not part of the default kubectl apply workflow. Every overlay there contains at least one site-specific value (CIDR, endpoint, label) that no shipped manifest can guess correctly across arbitrary clusters, so applying them unedited will fail the cluster into a state that is hard to debug.

Production guidance: the default install leaves the mirror listener (5000) and transfer listener (5001) reachable from other pods on the cluster network at <podIP>:port. The hostIP: 127.0.0.1 binding on the DaemonSet's hostPort only restricts host-side reach; the listener inside the pod is still 0.0.0.0. Production installs should adopt examples/networkpolicy.yaml.tmpl (or an equivalent NetworkPolicy in their own overlay) to close that pod-network gap. The overlay is shipped as an example rather than a default because its allow-list depends on the cluster's node-CIDR range, which is site-specific (see the workflow below).

examples/networkpolicy.yaml

Locks transfer (5001), libp2p (4001), mirror (5000), and metrics (9095) to the minimum traffic each port needs. Holds the manifest shape required by §7.5 but defers four CIDR choices to the operator - apiserver endpoint, kubelet probe source, mirror DNAT source, registry egress. See the long "OPERATOR ACTION REQUIRED" block at the top of the file and the Production caveats table below.

Workflow:

  1. Roll out the DaemonSet without the overlay and verify kubectl -n unbounded-system rollout status ds/gantry, p2p_cache_hit_total, and a successful workload pull.
  2. Copy the overlay into your own repository (or a Kustomize / Helm chart), edit every ipBlock marked "OPERATOR ACTION REQUIRED", and review against your CNI's hostPort SNAT behavior (kubectl get nodes -o yaml | grep -A2 podCIDR, etc.).
  3. Apply with kubectl apply -f your-overlay/networkpolicy.yaml. Watch /readyz and any in-flight mirror pulls for at least one full image pull cycle - a wrong CIDR will surface as dht routing table empty (no peer libp2p traffic) or as containerd connection refused on 5000 (wrong mirror source CIDR), not as a NetworkPolicy validation error.
  4. Roll back with kubectl delete networkpolicy -n unbounded-system gantry-agent if anything regresses.

Future hardening overlays (Pod Security Standards, dedicated PriorityClass, alternative hostNetwork: true topology) will live in the same directory and follow the same "deferred to operator, not in default install" rule.

Production caveats

A few configuration knobs that need operator attention before going to production:

Item Where What to change
API server egress CIDR examples/networkpolicy.yaml The egress to TCP/443 and TCP/6443 defaults to 0.0.0.0/0 because managed control planes (EKS / GKE / AKS) and self-hosted clusters reach the apiserver at IPs that don't match a namespaceSelector. Replace with the apiserver's actual CIDR - kubectl get endpoints kubernetes -n default -o jsonpath='{.subsets[*].addresses[*].ip}' for self-hosted clusters; the managed-service docs for hosted control planes.
Origin registry egress examples/networkpolicy.yaml The egress to TCP/443 for origin pulls also defaults to 0.0.0.0/0. If the cluster only pulls from a known set of registry endpoints (your private registry, ghcr.io, etc.), restrict this rule to those IPs or labels.
Kubelet probe source examples/networkpolicy.yaml Metrics ingress on TCP/9095 currently allows 0.0.0.0/0 so kubelet liveness/readiness probes (sourced from the node IP) reach the pod on strict CNIs. Replace with the node CIDR - kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'.
Mirror port 5000 source examples/networkpolicy.yaml Ingress on TCP/5000 defaults to a deliberately-narrow 127.0.0.1/32 placeholder. Most CNIs (Calico, Cilium, and managed offerings) SNAT hostPort traffic so the in-pod source-IP after DNAT is the node IP, NOT 127.0.0.1 - the placeholder will then drop containerd's mirror pulls. Replace with the node CIDR (same command as the kubelet probe row). MUST NOT widen to the pod-network CIDR: that bypasses the hostIP: 127.0.0.1 binding's loopback-only intent.
containerd socket access daemonset.yaml The pod mounts /run/containerd, rather than the socket file, so reconnects observe the replacement socket after containerd restarts. It runs with non-root UID 65532 and primary GID 0 because many nodes expose containerd.sock as root:root mode 0660. Validate this on your target node pool before production. If your runtime uses a dedicated socket group, patch runAsGroup/fsGroup to that group; if your policy forbids GID 0, adjust node socket ownership or run a site-specific privileged wrapper. Clearing containerd_socket is no longer a valid escape hatch - after plan-final-copilot-v2 §Phase 8 containerd is Gantry's sole storage backend; without socket access the agent has no content store to read from or write to. The storage_mode config value must remain containerd.
Kubernetes RBAC scope serviceaccount.yaml The agent's only Kubernetes access is leases (get, list, create, update) on the configured chair Leases in its own namespace and get on its own DaemonSet for capacity. It runs no informer and opens no watch, and it needs no access to Pods or Nodes. Review the Role to confirm scope hasn't drifted; a ClusterRole should not exist.
HEAD semantics on cache miss

GET /v2/<repo>/blobs/<digest> on a cache miss warms the cache as a side effect; HEAD on the same URL does NOT. This is intentional (see the comment block in internal/mirror/mirror.go at the HEAD return after writeBlobHeaders) - caching a multi-GB blob just because a client asked for its size would defeat the bandwidth amplification fix Gantry exists to provide. A subsequent GET for the same digest follows the cache-miss path normally and warms the cache then.

If your client emits HEAD-then-GET patterns where you'd prefer to amortize the origin metadata round-trip, raise the issue upstream (containerd's puller, BuildKit's resolver, etc.) - those clients generally have a one-shot resolve-and-pull mode that skips the HEAD entirely.

Documentation

Overview

Package gantry embeds the rendered gantry peer-to-peer OCI distribution manifests so they can be bundled into binaries that need to apply them (e.g. the unbounded-operator). Shared resources are sourced from chart/ and rendered with the operator values profile. Standalone support templates in this directory are added to the same rendered tree by `make gantry-manifests`; the operator filters those resources before applying.

The `all:` prefix in the embed directive plus the tracked rendered/.gitignore placeholder ensures the directive is satisfiable on a fresh clone (before `make gantry-manifests` has run), so Go tooling (`go build`, `go vet`, golangci-lint, gopls, ...) can load this package without requiring the rendering step to have happened first. The placeholder file is harmless at runtime: consumers that materialize the FS only apply *.yaml/*.yml files.

Index

Constants

This section is empty.

Variables

View Source
var Manifests = mustSub(manifestsRaw, "rendered")

Manifests exposes the rendered manifests as a filesystem rooted at the rendered/ directory, so consumers see the familiar layout (e.g. "daemonset.yaml", "configmap.yaml", "serviceaccount.yaml").

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL