fix(jumps): implement client-side field selector for persistent volume claims (#4079)

This commit is contained in:
Ümüt Özalp
2026-08-07 08:14:20 +02:00
committed by GitHub
parent b5d728c068
commit 3473560d44
3 changed files with 46 additions and 2 deletions
+1 -1
View File
@@ -875,7 +875,7 @@ Both `labelSelector` and `fieldSelector` support Go template syntax to dynamical
* `{{.spec.fieldName}}` - Any field from the source resource spec
* `{{.status.field}}` - Any field from the source resource status
> **Note:** The template above only controls the selector *value* (the right-hand side, computed from the source resource). The selector *key* is still validated by the Kubernetes API server against the **target** resource. Label selector keys can be any label, but **field selector keys are restricted**: for most resources only `metadata.name`/`metadata.namespace` are selectable, and for CRDs the target field must be declared in the CRD's `spec.versions[].selectableFields`. k9s passes the field selector straight to the API server (no local filtering), so referencing a non-selectable field returns `field label not supported`. See the Kubernetes docs on [CRD selectable fields](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#crd-selectable-fields).
> **Note:** The template above only controls the selector *value* (the right-hand side, computed from the source resource). For label selectors, the *key* can be any label and is matched server side. For **field selectors**, k9s applies the selector as a **local (client-side) filter** on the target resources, so the key may be any field path present in the target object (e.g. `metadata.name`, `spec.volumeName`), not just the API-selectable fields. This means field-selector jumps work even for resources whose fields are not server-side selectable. Field values are compared as strings, and the path is matched against the target object's manifest. For background on which fields the API server itself treats as selectable, see the Kubernetes docs on [CRD selectable fields](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#crd-selectable-fields).
### Examples
+41 -1
View File
@@ -6,8 +6,11 @@ package dao
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -30,7 +33,44 @@ func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error
lsel = sel
}
return r.getFactory().List(r.gvr, ns, false, lsel)
oo, err := r.getFactory().List(r.gvr, ns, false, lsel)
if err != nil {
return nil, err
}
return filterByFieldSelector(ctx, oo)
}
// filterByFieldSelector applies a client-side field selector if present in the context.
func filterByFieldSelector(ctx context.Context, oo []runtime.Object) ([]runtime.Object, error) {
sel, ok := ctx.Value(internal.KeyFields).(string)
if !ok || sel == "" {
return oo, nil
}
fsel, err := fields.ParseSelector(sel)
if err != nil {
return nil, err
}
res := make([]runtime.Object, 0, len(oo))
for _, o := range oo {
u, ok := o.(*unstructured.Unstructured)
if !ok {
// Cannot evaluate a field selector on a non-unstructured object.
res = append(res, o)
continue
}
set := make(fields.Set, len(fsel.Requirements()))
for _, req := range fsel.Requirements() {
val, _, _ := unstructured.NestedString(u.Object, strings.Split(req.Field, ".")...)
set[req.Field] = val
}
if fsel.Matches(set) {
res = append(res, o)
}
}
return res, nil
}
// Get returns a resource instance if found, else an error.
+4
View File
@@ -0,0 +1,4 @@
jumps:
"v1/persistentvolumeclaims":
targetGVR: "v1/persistentvolumes"
fieldSelector: "metadata.name={{.spec.volumeName}}"