Skip to content

Commit 24df901

Browse files
committed
feat(allowlist): support both v1 and v1alpha2 InferencePool APIs with auto-discovery
Signed-off-by: CYJiang <googs1025@gmail.com>
1 parent 1575b3a commit 24df901

File tree

1 file changed

+68
-14
lines changed

1 file changed

+68
-14
lines changed

pkg/sidecar/proxy/allowlist.go

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ import (
2424
"time"
2525

2626
"github.com/go-logr/logr"
27+
"k8s.io/apimachinery/pkg/api/errors"
2728
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2930
"k8s.io/apimachinery/pkg/labels"
3031
"k8s.io/apimachinery/pkg/runtime"
3132
"k8s.io/apimachinery/pkg/runtime/schema"
3233
"k8s.io/apimachinery/pkg/watch"
34+
"k8s.io/client-go/discovery"
3335
"k8s.io/client-go/dynamic"
3436
"k8s.io/client-go/tools/cache"
3537
"k8s.io/client-go/tools/clientcmd"
@@ -38,12 +40,28 @@ import (
3840
)
3941

4042
const (
41-
inferencePoolGroup = "inference.networking.x-k8s.io"
42-
inferencePoolVersion = "v1alpha2"
4343
inferencePoolResource = "inferencepools"
4444
resyncPeriod = 30 * time.Second
4545
)
4646

47+
// inferencePoolGVRCandidates in order of preference
48+
//
49+
// We maintain a prioritized list of GroupVersionResource (GVR) candidates to support
50+
// environments where either the legacy or the new InferencePool CRD may be installed:
51+
//
52+
// 1. inference.networking.k8s.io/v1 ← Preferred (new official API group)
53+
// 2. inference.networking.x-k8s.io/v1alpha2 ← Fallback (legacy experimental API group)
54+
55+
// The validator automatically detects which API is available by using the Kubernetes
56+
// discovery API, selecting the first supported GVR in this list.
57+
//
58+
// This approach aligns with upstream Ingress Gateway (IGW) behavior, which also supports
59+
// both API versions concurrently (see issue #462).
60+
var inferencePoolGVRCandidates = []schema.GroupVersionResource{
61+
{Group: "inference.networking.k8s.io", Version: "v1", Resource: inferencePoolResource},
62+
{Group: "inference.networking.x-k8s.io", Version: "v1alpha2", Resource: inferencePoolResource},
63+
}
64+
4765
// AllowlistValidator manages allowed prefill targets based on InferencePool resources
4866
type AllowlistValidator struct {
4967
logger logr.Logger
@@ -52,6 +70,8 @@ type AllowlistValidator struct {
5270
poolName string
5371
enabled bool
5472

73+
gvr schema.GroupVersionResource // detected GVR
74+
5575
// allowedTargets maps hostport -> bool for allowed prefill targets
5676
allowedTargets set.Set[string]
5777
allowedTargetsMu sync.RWMutex
@@ -87,6 +107,27 @@ func NewAllowlistValidator(enabled bool, namespace string, poolName string) (*Al
87107
return nil, fmt.Errorf("failed to create Kubernetes dynamic client: %w", err)
88108
}
89109

110+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
111+
if err != nil {
112+
return nil, fmt.Errorf("failed to create discovery client: %w", err)
113+
}
114+
115+
var detectedGVR schema.GroupVersionResource
116+
for _, gvr := range inferencePoolGVRCandidates {
117+
supported, err := isGVRSupported(discoveryClient, gvr)
118+
if err != nil {
119+
return nil, fmt.Errorf("error checking GVR %s: %w", gvr.String(), err)
120+
}
121+
if supported {
122+
detectedGVR = gvr
123+
break
124+
}
125+
}
126+
127+
if detectedGVR.Empty() {
128+
return nil, fmt.Errorf("no supported InferencePool API found; tried: %v", inferencePoolGVRCandidates)
129+
}
130+
90131
return &AllowlistValidator{
91132
enabled: true,
92133
dynamicClient: dynamicClient,
@@ -99,32 +140,45 @@ func NewAllowlistValidator(enabled bool, namespace string, poolName string) (*Al
99140
}, nil
100141
}
101142

143+
func isGVRSupported(discoveryClient discovery.DiscoveryInterface, gvr schema.GroupVersionResource) (bool, error) {
144+
apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(gvr.GroupVersion().String())
145+
if err != nil {
146+
// If the group/version doesn't exist, Kubernetes returns a "NotFound" error
147+
if errors.IsNotFound(err) {
148+
return false, nil // GroupVersion not supported
149+
}
150+
return false, fmt.Errorf("failed to discover resources for %s: %w", gvr.GroupVersion(), err)
151+
}
152+
153+
for _, resource := range apiResourceList.APIResources {
154+
if resource.Name == gvr.Resource {
155+
return true, nil
156+
}
157+
}
158+
return false, nil
159+
}
160+
102161
// Start begins watching InferencePool resources and managing the allowlist
103162
func (av *AllowlistValidator) Start(ctx context.Context) error {
104163
if !av.enabled {
105164
return nil
106165
}
107166

108167
av.logger = log.FromContext(ctx).WithName("allowlist-validator")
109-
av.logger.Info("starting SSRF protection allowlist validator", "namespace", av.namespace, "poolName", av.poolName)
110-
111-
gvr := schema.GroupVersionResource{
112-
Group: inferencePoolGroup,
113-
Version: inferencePoolVersion,
114-
Resource: inferencePoolResource,
115-
}
168+
av.logger.Info("starting SSRF protection allowlist validator",
169+
"namespace", av.namespace, "poolName", av.poolName, "gvr", av.gvr.String())
116170

117171
// Create informer for the specific InferencePool resource
118172
lw := &cache.ListWatch{
119-
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
173+
ListWithContextFunc: func(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) {
120174
// List with field selector to get only the specific InferencePool
121175
options.FieldSelector = "metadata.name=" + av.poolName
122-
return av.dynamicClient.Resource(gvr).Namespace(av.namespace).List(ctx, options)
176+
return av.dynamicClient.Resource(av.gvr).Namespace(av.namespace).List(ctx, options)
123177
},
124-
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
178+
WatchFuncWithContext: func(ctx context.Context, options metav1.ListOptions) (watch.Interface, error) {
125179
// Watch the specific InferencePool by name using field selector
126180
options.FieldSelector = "metadata.name=" + av.poolName
127-
return av.dynamicClient.Resource(gvr).Namespace(av.namespace).Watch(ctx, options)
181+
return av.dynamicClient.Resource(av.gvr).Namespace(av.namespace).Watch(ctx, options)
128182
},
129183
}
130184

@@ -142,7 +196,7 @@ func (av *AllowlistValidator) Start(ctx context.Context) error {
142196

143197
// Wait for cache sync
144198
if !cache.WaitForCacheSync(av.stopCh, av.poolInformer.HasSynced) {
145-
return fmt.Errorf("failed to sync InferencePool cache within timeout (check RBAC permissions for inferencepools.%s and that pool '%s' exists)", inferencePoolGroup, av.poolName)
199+
return fmt.Errorf("failed to sync InferencePool cache within timeout (check RBAC permissions for inferencepools.%s and that pool '%s' exists)", av.gvr.String(), av.poolName)
146200
}
147201

148202
av.logger.Info("allowlist validator started successfully")

0 commit comments

Comments
 (0)