Skip to content

Commit de0f2ef

Browse files
dilyevskyclaude
andcommitted
[cli,apiserver] add --field-selector flag to all list commands and --zone shortcut on domain
Wire server-side field selector support (customGetAttrs) into the apiserver storage layer and expose it via a generic --field-selector flag on every resource list command. Add a ListFlags hook to ResourceCommand so individual resources can contribute convenience flags — used here for domain's --zone flag which maps to spec.zone=<value>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f6e492 commit de0f2ef

File tree

6 files changed

+359
-2
lines changed

6 files changed

+359
-2
lines changed

pkg/apiserver/fieldselectors.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package apiserver
2+
3+
import (
4+
"fmt"
5+
6+
"k8s.io/apimachinery/pkg/fields"
7+
"k8s.io/apimachinery/pkg/labels"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
"k8s.io/apiserver/pkg/registry/generic"
10+
"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
11+
12+
corev1alpha2 "github.com/apoxy-dev/apoxy/api/core/v1alpha2"
13+
)
14+
15+
// customGetAttrs returns labels and fields for field selector filtering.
16+
// It extends the default metadata.name with per-type custom fields.
17+
func customGetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
18+
provider, ok := obj.(resource.Object)
19+
if !ok {
20+
return nil, nil, fmt.Errorf("object of type %T does not implement resource.Object", obj)
21+
}
22+
om := provider.GetObjectMeta()
23+
fs := generic.ObjectMetaFieldsSet(om, false) // non-namespaced
24+
25+
switch o := obj.(type) {
26+
case *corev1alpha2.Domain:
27+
fs["spec.zone"] = o.Spec.Zone
28+
fs["status.phase"] = string(o.Status.Phase)
29+
case *corev1alpha2.DomainZone:
30+
fs["status.phase"] = string(o.Status.Phase)
31+
case *corev1alpha2.Proxy:
32+
fs["spec.provider"] = string(o.Spec.Provider)
33+
case *corev1alpha2.Backend:
34+
fs["spec.protocol"] = string(o.Spec.Protocol)
35+
}
36+
37+
return labels.Set(om.Labels), fs, nil
38+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package apiserver
2+
3+
import (
4+
"testing"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
corev1alpha2 "github.com/apoxy-dev/apoxy/api/core/v1alpha2"
11+
)
12+
13+
func TestCustomGetAttrs_Domain(t *testing.T) {
14+
domain := &corev1alpha2.Domain{
15+
ObjectMeta: metav1.ObjectMeta{
16+
Name: "test-domain",
17+
Labels: map[string]string{"app": "web"},
18+
},
19+
Spec: corev1alpha2.DomainSpec{
20+
Zone: "example.com",
21+
},
22+
Status: corev1alpha2.DomainStatus{
23+
Phase: corev1alpha2.DomainPhaseActive,
24+
},
25+
}
26+
27+
lbls, fs, err := customGetAttrs(domain)
28+
require.NoError(t, err)
29+
30+
assert.Equal(t, "web", lbls["app"])
31+
assert.Equal(t, "test-domain", fs["metadata.name"])
32+
assert.Equal(t, "example.com", fs["spec.zone"])
33+
assert.Equal(t, "Active", fs["status.phase"])
34+
}
35+
36+
func TestCustomGetAttrs_DomainZone(t *testing.T) {
37+
dz := &corev1alpha2.DomainZone{
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: "example.com",
40+
},
41+
Status: corev1alpha2.DomainZoneStatus{
42+
Phase: corev1alpha2.DomainZonePhaseActive,
43+
},
44+
}
45+
46+
_, fs, err := customGetAttrs(dz)
47+
require.NoError(t, err)
48+
49+
assert.Equal(t, "example.com", fs["metadata.name"])
50+
assert.Equal(t, "Active", fs["status.phase"])
51+
}
52+
53+
func TestCustomGetAttrs_Proxy(t *testing.T) {
54+
proxy := &corev1alpha2.Proxy{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Name: "my-proxy",
57+
},
58+
Spec: corev1alpha2.ProxySpec{
59+
Provider: corev1alpha2.InfraProviderCloud,
60+
},
61+
}
62+
63+
_, fs, err := customGetAttrs(proxy)
64+
require.NoError(t, err)
65+
66+
assert.Equal(t, "my-proxy", fs["metadata.name"])
67+
assert.Equal(t, "cloud", fs["spec.provider"])
68+
}
69+
70+
func TestCustomGetAttrs_Backend(t *testing.T) {
71+
backend := &corev1alpha2.Backend{
72+
ObjectMeta: metav1.ObjectMeta{
73+
Name: "my-backend",
74+
},
75+
Spec: corev1alpha2.BackendSpec{
76+
Protocol: corev1alpha2.BackendProtoH2,
77+
},
78+
}
79+
80+
_, fs, err := customGetAttrs(backend)
81+
require.NoError(t, err)
82+
83+
assert.Equal(t, "my-backend", fs["metadata.name"])
84+
assert.Equal(t, "h2", fs["spec.protocol"])
85+
}
86+
87+
func TestCustomGetAttrs_EmptyFields(t *testing.T) {
88+
domain := &corev1alpha2.Domain{
89+
ObjectMeta: metav1.ObjectMeta{
90+
Name: "empty",
91+
},
92+
}
93+
94+
_, fs, err := customGetAttrs(domain)
95+
require.NoError(t, err)
96+
97+
assert.Equal(t, "empty", fs["metadata.name"])
98+
assert.Equal(t, "", fs["spec.zone"])
99+
assert.Equal(t, "", fs["status.phase"])
100+
}

pkg/apiserver/storage.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ func NewKineStorage(ctx context.Context, dbPath string, connArgs map[string]stri
206206
etcdConfig: etcdConfig,
207207
groupVersioner: s.StorageVersioner,
208208
}
209+
options.AttrFunc = customGetAttrs
209210
}, nil
210211
}
211212

pkg/cmd/domain/domain.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ var domainResource = &resource.ResourceCommand[*corev1alpha2.Domain, *corev1alph
2121
ObjToTable: func(d *corev1alpha2.Domain) resource.TableConverter { return d },
2222
ListToTable: func(l *corev1alpha2.DomainList) resource.TableConverter { return l },
2323
},
24+
ListFlags: func(cmd *cobra.Command) func() string {
25+
var zone string
26+
cmd.Flags().StringVar(&zone, "zone", "", "Filter domains by zone name.")
27+
return func() string {
28+
if zone != "" {
29+
return "spec.zone=" + zone
30+
}
31+
return ""
32+
}
33+
},
2434
}
2535

2636
var zoneResource = &resource.ResourceCommand[*corev1alpha2.DomainZone, *corev1alpha2.DomainZoneList]{

pkg/cmd/resource/builder.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ type ResourceCommand[T Object, TList runtime.Object] struct {
6868

6969
// PostGet is an optional hook called after Get to display additional information.
7070
PostGet func(ctx context.Context, c *rest.APIClient, name string, obj T) error
71+
72+
// ListFlags registers custom flags on both the root and list commands.
73+
// Returns a function that produces a field selector string from those flags.
74+
ListFlags func(cmd *cobra.Command) func() string
7175
}
7276

7377
func (r *ResourceCommand[T, TList]) printObj(ctx context.Context, obj T, showLabels bool) error {
@@ -115,12 +119,16 @@ func (r *ResourceCommand[T, TList]) printList(ctx context.Context, list TList, s
115119
func (r *ResourceCommand[T, TList]) Build() *cobra.Command {
116120
var (
117121
showLabels bool
122+
fieldSelector string
118123
createFile string
119124
applyFile string
120125
fieldManager string
121126
forceConflicts bool
122127
)
123128

129+
// Register ListFlags on root and list commands; hold the closures for runtime.
130+
var rootListFlagsFn, listListFlagsFn func() string
131+
124132
rootCmd := &cobra.Command{
125133
Use: r.Use,
126134
Short: r.Short,
@@ -132,7 +140,17 @@ func (r *ResourceCommand[T, TList]) Build() *cobra.Command {
132140
if err != nil {
133141
return err
134142
}
135-
list, err := r.ClientFunc(c).List(cmd.Context(), metav1.ListOptions{})
143+
fs := fieldSelector
144+
if rootListFlagsFn != nil {
145+
if extra := rootListFlagsFn(); extra != "" {
146+
if fs != "" {
147+
fs += "," + extra
148+
} else {
149+
fs = extra
150+
}
151+
}
152+
}
153+
list, err := r.ClientFunc(c).List(cmd.Context(), metav1.ListOptions{FieldSelector: fs})
136154
if err != nil {
137155
return err
138156
}
@@ -174,7 +192,17 @@ func (r *ResourceCommand[T, TList]) Build() *cobra.Command {
174192
if err != nil {
175193
return err
176194
}
177-
list, err := r.ClientFunc(c).List(cmd.Context(), metav1.ListOptions{})
195+
fs := fieldSelector
196+
if listListFlagsFn != nil {
197+
if extra := listListFlagsFn(); extra != "" {
198+
if fs != "" {
199+
fs += "," + extra
200+
} else {
201+
fs = extra
202+
}
203+
}
204+
}
205+
list, err := r.ClientFunc(c).List(cmd.Context(), metav1.ListOptions{FieldSelector: fs})
178206
if err != nil {
179207
return err
180208
}
@@ -300,12 +328,19 @@ manage different fields of the same object without conflicts.`, r.KindName, r.Ki
300328
}
301329

302330
// Register flags.
331+
rootCmd.Flags().StringVar(&fieldSelector, "field-selector", "", "Filter list results by field selectors (e.g. spec.zone=example.com).")
332+
listCmd.Flags().StringVar(&fieldSelector, "field-selector", "", "Filter list results by field selectors (e.g. spec.zone=example.com).")
303333
createCmd.Flags().StringVarP(&createFile, "filename", "f", "", "The file that contains the configuration to create.")
304334
listCmd.Flags().BoolVar(&showLabels, "show-labels", false, fmt.Sprintf("Print the %s's labels.", r.KindName))
305335
applyCmd.Flags().StringVarP(&applyFile, "filename", "f", "", "The file that contains the configuration to apply.")
306336
applyCmd.Flags().StringVar(&fieldManager, "field-manager", "apoxy-cli", "Name of the field manager for server-side apply.")
307337
applyCmd.Flags().BoolVar(&forceConflicts, "force-conflicts", false, "Force apply even if there are field ownership conflicts.")
308338

339+
if r.ListFlags != nil {
340+
rootListFlagsFn = r.ListFlags(rootCmd)
341+
listListFlagsFn = r.ListFlags(listCmd)
342+
}
343+
309344
rootCmd.AddCommand(getCmd, listCmd, createCmd, deleteCmd, applyCmd)
310345
return rootCmd
311346
}

0 commit comments

Comments
 (0)