@@ -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+ // candidateGVRs 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 candidateGVRs = []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,44 +107,79 @@ 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 candidateGVRs {
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" , candidateGVRs )
129+ }
130+
90131 return & AllowlistValidator {
91132 enabled : true ,
92133 dynamicClient : dynamicClient ,
93134 namespace : namespace ,
94135 poolName : poolName ,
136+ gvr : detectedGVR ,
95137 allowedTargets : set .New [string ](),
96138 podInformers : make (map [string ]cache.SharedInformer ),
97139 podStopChans : make (map [string ]chan struct {}),
98140 stopCh : make (chan struct {}),
99141 }, nil
100142}
101143
144+ func isGVRSupported (discoveryClient discovery.DiscoveryInterface , gvr schema.GroupVersionResource ) (bool , error ) {
145+ apiResourceList , err := discoveryClient .ServerResourcesForGroupVersion (gvr .GroupVersion ().String ())
146+ if err != nil {
147+ // If the group/version doesn't exist, Kubernetes returns a "NotFound" error
148+ if errors .IsNotFound (err ) {
149+ return false , nil // GroupVersion not supported
150+ }
151+ return false , fmt .Errorf ("failed to discover resources for %s: %w" , gvr .String (), err )
152+ }
153+
154+ for _ , resource := range apiResourceList .APIResources {
155+ if resource .Name == gvr .Resource {
156+ return true , nil
157+ }
158+ }
159+ return false , nil
160+ }
161+
102162// Start begins watching InferencePool resources and managing the allowlist
103163func (av * AllowlistValidator ) Start (ctx context.Context ) error {
104164 if ! av .enabled {
105165 return nil
106166 }
107167
108168 av .logger = klog .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- }
169+ av .logger .Info ("starting SSRF protection allowlist validator" ,
170+ "namespace" , av .namespace , "poolName" , av .poolName , "gvr" , av .gvr .String ())
116171
117172 // Create informer for the specific InferencePool resource
118173 lw := & cache.ListWatch {
119- ListFunc : func (options metav1.ListOptions ) (runtime.Object , error ) {
174+ ListWithContextFunc : func (ctx context. Context , options metav1.ListOptions ) (runtime.Object , error ) {
120175 // List with field selector to get only the specific InferencePool
121176 options .FieldSelector = "metadata.name=" + av .poolName
122- return av .dynamicClient .Resource (gvr ).Namespace (av .namespace ).List (ctx , options )
177+ return av .dynamicClient .Resource (av . gvr ).Namespace (av .namespace ).List (ctx , options )
123178 },
124- WatchFunc : func (options metav1.ListOptions ) (watch.Interface , error ) {
179+ WatchFuncWithContext : func (ctx context. Context , options metav1.ListOptions ) (watch.Interface , error ) {
125180 // Watch the specific InferencePool by name using field selector
126181 options .FieldSelector = "metadata.name=" + av .poolName
127- return av .dynamicClient .Resource (gvr ).Namespace (av .namespace ).Watch (ctx , options )
182+ return av .dynamicClient .Resource (av . gvr ).Namespace (av .namespace ).Watch (ctx , options )
128183 },
129184 }
130185
@@ -142,7 +197,7 @@ func (av *AllowlistValidator) Start(ctx context.Context) error {
142197
143198 // Wait for cache sync
144199 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 )
200+ 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 )
146201 }
147202
148203 av .logger .Info ("allowlist validator started successfully" )
0 commit comments