Skip to content

Commit 4c16a44

Browse files
authored
fix(sidecar): allowlist selector parsing for GA InferencePool API (#1696)
The GA InferencePool API (inference.networking.k8s.io/v1) nests pod labels under spec.selector.matchLabels, while the deprecated alpha API (inference.networking.x-k8s.io/v1alpha2) uses a flat spec.selector map. The allowlist validator read spec.selector unconditionally and fmt.Sprintf'd nested map values into label strings, producing invalid selectors like "map[app.kubernetes.io/name:...]" that Kubernetes rejected for exceeding the 63-character label value limit. Branch the selector path based on the detected API group and use NestedStringMap for type-safe extraction. Signed-off-by: Pierangelo Di Pilato <pierdipi@redhat.com>
1 parent 48b7dff commit 4c16a44

2 files changed

Lines changed: 155 additions & 13 deletions

File tree

pkg/sidecar/proxy/allowlist.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -250,27 +250,34 @@ func (av *AllowlistValidator) onInferencePoolDelete(obj interface{}) {
250250
func (av *AllowlistValidator) updatePodsForPool(poolObj *unstructured.Unstructured) {
251251
poolName := poolObj.GetName()
252252

253-
// Parse the pool spec to get selector
254-
spec, found, err := unstructured.NestedMap(poolObj.Object, "spec")
255-
if err != nil || !found {
256-
av.logger.Error(err, "InferencePool missing or invalid spec field", "name", poolName, "found", found)
253+
selector, err := av.poolSelector(poolObj)
254+
if err != nil {
255+
av.logger.Error(err, "failed to extract selector from InferencePool", "name", poolName)
257256
return
258257
}
259258

260-
selectorData, found, err := unstructured.NestedMap(spec, "selector")
259+
av.createPodInformer(poolName, selector)
260+
}
261+
262+
func (av *AllowlistValidator) poolSelector(poolObj *unstructured.Unstructured) (labels.Selector, error) {
263+
spec, found, err := unstructured.NestedMap(poolObj.Object, "spec")
261264
if err != nil || !found {
262-
av.logger.Error(err, "InferencePool missing or invalid selector field", "name", poolName, "found", found)
263-
return
265+
return nil, fmt.Errorf("missing or invalid spec field (found=%t): %w", found, err)
264266
}
265267

266-
// Convert to labels.Selector
267-
labelSelector := labels.Set{}
268-
for k, v := range selectorData {
269-
labelSelector[k] = fmt.Sprintf("%v", v)
268+
// GA API (inference.networking.k8s.io) uses spec.selector.matchLabels;
269+
// deprecated alpha API (inference.networking.x-k8s.io) uses a flat spec.selector map.
270+
selectorPath := []string{"selector", "matchLabels"}
271+
if av.gvr.Group != routing.InferencePoolAPIGroup {
272+
selectorPath = []string{"selector"}
273+
}
274+
275+
selectorData, found, err := unstructured.NestedStringMap(spec, selectorPath...)
276+
if err != nil || !found {
277+
return nil, fmt.Errorf("missing or invalid selector field at %v (found=%t): %w", selectorPath, found, err)
270278
}
271279

272-
// Create or update pod informer for this selector
273-
av.createPodInformer(poolName, labelSelector.AsSelector())
280+
return labels.Set(selectorData).AsSelector(), nil
274281
}
275282

276283
// createPodInformer creates a new pod informer for the given selector

pkg/sidecar/proxy/allowlist_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
. "github.com/onsi/gomega" // nolint:revive
2222

2323
"github.com/llm-d/llm-d-router/pkg/common/routing"
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
2426
"k8s.io/utils/set"
2527
)
2628

@@ -41,6 +43,139 @@ var _ = Describe("AllowlistValidator", func() {
4143
})
4244
})
4345

46+
Context("poolSelector", func() {
47+
It("should extract selector from GA InferencePool (matchLabels)", func() {
48+
av := &AllowlistValidator{
49+
gvr: schema.GroupVersionResource{
50+
Group: routing.InferencePoolAPIGroup,
51+
Version: "v1",
52+
Resource: "inferencepools",
53+
},
54+
}
55+
pool := &unstructured.Unstructured{
56+
Object: map[string]interface{}{
57+
"apiVersion": "inference.networking.k8s.io/v1",
58+
"kind": "InferencePool",
59+
"metadata": map[string]interface{}{"name": "test-pool"},
60+
"spec": map[string]interface{}{
61+
"selector": map[string]interface{}{
62+
"matchLabels": map[string]interface{}{
63+
"app.kubernetes.io/name": "my-model",
64+
"component": "serving",
65+
},
66+
},
67+
},
68+
},
69+
}
70+
71+
selector, err := av.poolSelector(pool)
72+
Expect(err).ToNot(HaveOccurred())
73+
Expect(selector.String()).To(SatisfyAll(
74+
ContainSubstring("app.kubernetes.io/name=my-model"),
75+
ContainSubstring("component=serving"),
76+
))
77+
})
78+
79+
It("should extract selector from deprecated alpha InferencePool (flat map)", func() {
80+
av := &AllowlistValidator{
81+
gvr: schema.GroupVersionResource{
82+
Group: "inference.networking.x-k8s.io",
83+
Version: "v1alpha2",
84+
Resource: "inferencepools",
85+
},
86+
}
87+
pool := &unstructured.Unstructured{
88+
Object: map[string]interface{}{
89+
"apiVersion": "inference.networking.x-k8s.io/v1alpha2",
90+
"kind": "InferencePool",
91+
"metadata": map[string]interface{}{"name": "test-pool"},
92+
"spec": map[string]interface{}{
93+
"selector": map[string]interface{}{
94+
"app.kubernetes.io/name": "my-model",
95+
"component": "serving",
96+
},
97+
},
98+
},
99+
}
100+
101+
selector, err := av.poolSelector(pool)
102+
Expect(err).ToNot(HaveOccurred())
103+
Expect(selector.String()).To(SatisfyAll(
104+
ContainSubstring("app.kubernetes.io/name=my-model"),
105+
ContainSubstring("component=serving"),
106+
))
107+
})
108+
109+
It("should fail for GA pool with flat selector (no matchLabels)", func() {
110+
av := &AllowlistValidator{
111+
gvr: schema.GroupVersionResource{
112+
Group: routing.InferencePoolAPIGroup,
113+
Version: "v1",
114+
Resource: "inferencepools",
115+
},
116+
}
117+
pool := &unstructured.Unstructured{
118+
Object: map[string]interface{}{
119+
"apiVersion": "inference.networking.k8s.io/v1",
120+
"kind": "InferencePool",
121+
"metadata": map[string]interface{}{"name": "test-pool"},
122+
"spec": map[string]interface{}{
123+
"selector": map[string]interface{}{
124+
"app": "my-model",
125+
},
126+
},
127+
},
128+
}
129+
130+
_, err := av.poolSelector(pool)
131+
Expect(err).To(HaveOccurred())
132+
Expect(err.Error()).To(ContainSubstring("matchLabels"))
133+
})
134+
135+
It("should fail when spec is missing", func() {
136+
av := &AllowlistValidator{
137+
gvr: schema.GroupVersionResource{
138+
Group: routing.InferencePoolAPIGroup,
139+
Version: "v1",
140+
Resource: "inferencepools",
141+
},
142+
}
143+
pool := &unstructured.Unstructured{
144+
Object: map[string]interface{}{
145+
"apiVersion": "inference.networking.k8s.io/v1",
146+
"kind": "InferencePool",
147+
"metadata": map[string]interface{}{"name": "test-pool"},
148+
},
149+
}
150+
151+
_, err := av.poolSelector(pool)
152+
Expect(err).To(HaveOccurred())
153+
Expect(err.Error()).To(ContainSubstring("spec"))
154+
})
155+
156+
It("should fail when selector is missing", func() {
157+
av := &AllowlistValidator{
158+
gvr: schema.GroupVersionResource{
159+
Group: "inference.networking.x-k8s.io",
160+
Version: "v1alpha2",
161+
Resource: "inferencepools",
162+
},
163+
}
164+
pool := &unstructured.Unstructured{
165+
Object: map[string]interface{}{
166+
"apiVersion": "inference.networking.x-k8s.io/v1alpha2",
167+
"kind": "InferencePool",
168+
"metadata": map[string]interface{}{"name": "test-pool"},
169+
"spec": map[string]interface{}{},
170+
},
171+
}
172+
173+
_, err := av.poolSelector(pool)
174+
Expect(err).To(HaveOccurred())
175+
Expect(err.Error()).To(ContainSubstring("selector"))
176+
})
177+
})
178+
44179
Context("when SSRF protection is enabled", func() {
45180
var validator *AllowlistValidator
46181

0 commit comments

Comments
 (0)