Skip to content

Commit 70ebbf1

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

File tree

1 file changed

+69
-14
lines changed

1 file changed

+69
-14
lines changed

pkg/sidecar/proxy/allowlist.go

Lines changed: 69 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+
// 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
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,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
103163
func (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

Comments
 (0)