Discovery
Inside the cluster, Octopus discovers upstream Pod endpoints from the Kubernetes API. The primary
path uses the discovery.k8s.io/v1 EndpointSlice API, which reflects Pod scale up/down — events
a Service-only watch would miss. The legacy Endpoints API is retained as a fallback for clusters
where the EndpointSlice API is unavailable.
This is configured as a FARP discovery backend of type kubernetes. It is independent of the
operator: the operator programs routes from Gateway API / Octopus
CRDs, while discovery populates upstream instances.
How it works01
It lists
Serviceobjects (honoring the label selector) and the EndpointSlices that own them (matched via thekubernetes.io/service-namelabel), then groups slices by their owning(namespace, service).For each Service that passed the selector, it emits one upstream instance per ready endpoint address × port. Slices of
addressType: FQDNare skipped (no routable IPs).A watch on EndpointSlices keeps instances current: applied/updated slices re-discover the owning Service, deleted slices deregister it. Because slices change on scale events, scaling a Deployment up or down is reflected without a Service event.
Each endpoint's
readycondition maps to instance health:ready: true→ healthy,false→ unhealthy, unset → unknown.
Configuration02
Discovery is enabled under farp.discovery.backends as a kubernetes backend:
farp:
enabled: true
discovery:
backends:
- type: kubernetes
enabled: true
config:
# "" = watch all namespaces (needs cluster-scoped RBAC).
namespace: ""
# label_selector: "app.kubernetes.io/part-of=my-platform"
watch_interval: 30s
use_endpoint_slices: true
include_not_ready: false| Field | Type | Default | Description |
namespace | string | (required) | Namespace to watch. Empty string watches all namespaces (requires cluster-scoped RBAC). |
label_selector | string | (unset) | Label selector filtering which Services are discovered. |
watch_interval | duration | (required) | Resync/poll interval. |
use_endpoint_slices | bool | true | Prefer the EndpointSlice API; fall back to Endpoints if it's unavailable. |
include_not_ready | bool | false | Also emit endpoints whose ready condition is false/unknown. |
When namespace is empty (all namespaces) the discovery client lists/watches cluster-wide; when
set, it scopes the API calls to that one namespace. Single-namespace discovery only needs a
namespaced Role.
include_not_ready03
By default only ready endpoints become upstream instances, so traffic is never sent to a Pod
that is still starting or failing its readiness probe. Set include_not_ready: true to also surface
not-ready endpoints (for example, to drive your own health-aware load balancing); they are emitted
with unhealthy/unknown health rather than being dropped.
EndpointSlice vs. Endpoints fallback04
With use_endpoint_slices: true (the default), discovery runs the EndpointSlice path and only falls
back to the legacy Endpoints API if that path errors (e.g. the API is not served). The
EndpointSlice path is preferred because it reflects scale up/down; the Endpoints fallback lists
ready addresses per Service subset and treats all listed addresses as healthy.
RBAC05
Discovery needs get/list/watch on the following. The Helm chart's RBAC already grants these:
- apiGroups: [""]
resources: ["services", "endpoints"] # Service metadata + legacy Endpoints fallback
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"] # primary discovery source
verbs: ["get", "list", "watch"]To watch across all namespaces you need a ClusterRole/ClusterRoleBinding
(rbac.clusterScoped=true, the chart default). To restrict discovery to one namespace, set
rbac.clusterScoped=false and discovery.namespace=<ns>; the chart then creates a namespaced
Role/RoleBinding covering just services, endpoints, and endpointslices. You can confirm
access with:
kubectl auth can-i list endpointslices \
--as=system:serviceaccount:octopus:octopusSee Probes & drain for how the first discovery sync gates readiness, and the Helm chart page for the RBAC and discovery-scope values.