@@ -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
4042const (
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
4866type 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
103162func (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