@@ -18,10 +18,12 @@ import (
1818 "github.com/zxh326/kite/pkg/rbac"
1919 corev1 "k8s.io/api/core/v1"
2020 apierrors "k8s.io/apimachinery/pkg/api/errors"
21+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2122 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2223 "k8s.io/apimachinery/pkg/labels"
2324 "k8s.io/apimachinery/pkg/runtime/schema"
2425 k8stypes "k8s.io/apimachinery/pkg/types"
26+ "k8s.io/client-go/discovery"
2527 "k8s.io/klog/v2"
2628 "sigs.k8s.io/controller-runtime/pkg/client"
2729 "sigs.k8s.io/yaml"
@@ -226,7 +228,7 @@ type resourceInfo struct {
226228 ClusterScoped bool
227229}
228230
229- func resolveResourceInfo (kind string ) resourceInfo {
231+ func resolveStaticResourceInfo (kind string ) resourceInfo {
230232 switch strings .ToLower (strings .TrimSpace (kind )) {
231233 case "pod" , "pods" :
232234 return resourceInfo {Kind : "Pod" , Resource : "pods" , Version : "v1" }
@@ -264,6 +266,8 @@ func resolveResourceInfo(kind string) resourceInfo {
264266 return resourceInfo {Kind : "NetworkPolicy" , Resource : "networkpolicies" , Group : "networking.k8s.io" , Version : "v1" }
265267 case "storageclass" , "storageclasses" , "sc" :
266268 return resourceInfo {Kind : "StorageClass" , Resource : "storageclasses" , Group : "storage.k8s.io" , Version : "v1" , ClusterScoped : true }
269+ case "customresourcedefinition" , "customresourcedefinitions" , "crd" , "crds" :
270+ return resourceInfo {Kind : "CustomResourceDefinition" , Resource : "customresourcedefinitions" , Group : "apiextensions.k8s.io" , Version : "v1" , ClusterScoped : true }
267271 case "event" , "events" :
268272 return resourceInfo {Kind : "Event" , Resource : "events" , Version : "v1" }
269273 default :
@@ -284,6 +288,131 @@ func resolveResourceInfo(kind string) resourceInfo {
284288 }
285289}
286290
291+ func resolveResourceInfo (ctx context.Context , cs * cluster.ClientSet , kind string ) resourceInfo {
292+ if info , ok := resolveResourceInfoFromDiscovery (ctx , cs , kind , "" ); ok {
293+ return info
294+ }
295+ return resolveStaticResourceInfo (kind )
296+ }
297+
298+ func resolveResourceInfoForObject (ctx context.Context , cs * cluster.ClientSet , obj * unstructured.Unstructured ) resourceInfo {
299+ if info , ok := resolveResourceInfoFromDiscovery (ctx , cs , obj .GetKind (), obj .GetAPIVersion ()); ok {
300+ return info
301+ }
302+ return resolveStaticResourceInfo (obj .GetKind ())
303+ }
304+
305+ func resolveResourceInfoFromDiscovery (ctx context.Context , cs * cluster.ClientSet , kind , apiVersion string ) (resourceInfo , bool ) {
306+ input := strings .ToLower (strings .TrimSpace (kind ))
307+ if input == "" || cs == nil || cs .K8sClient == nil || cs .K8sClient .ClientSet == nil {
308+ return resourceInfo {}, false
309+ }
310+ if ctx != nil {
311+ select {
312+ case <- ctx .Done ():
313+ return resourceInfo {}, false
314+ default :
315+ }
316+ }
317+ discoveryClient := cs .K8sClient .ClientSet .Discovery ()
318+
319+ if gv , ok := parseGroupVersion (apiVersion ); ok {
320+ resourceList , err := discoveryClient .ServerResourcesForGroupVersion (gv .String ())
321+ if err != nil {
322+ klog .V (2 ).Infof ("AI tool discovery failed for %s: %v" , gv .String (), err )
323+ } else if info , found := findResourceInfoInList (input , gv , resourceList .APIResources ); found {
324+ return info , true
325+ }
326+ }
327+
328+ resourceLists , err := discoveryClient .ServerPreferredResources ()
329+ if err != nil && ! discovery .IsGroupDiscoveryFailedError (err ) {
330+ klog .V (2 ).Infof ("AI tool preferred discovery failed: %v" , err )
331+ return resourceInfo {}, false
332+ }
333+
334+ for _ , resourceList := range resourceLists {
335+ if resourceList == nil {
336+ continue
337+ }
338+ gv , err := schema .ParseGroupVersion (resourceList .GroupVersion )
339+ if err != nil {
340+ continue
341+ }
342+ if info , found := findResourceInfoInList (input , gv , resourceList .APIResources ); found {
343+ return info , true
344+ }
345+ }
346+
347+ return resourceInfo {}, false
348+ }
349+
350+ func parseGroupVersion (apiVersion string ) (schema.GroupVersion , bool ) {
351+ apiVersion = strings .TrimSpace (apiVersion )
352+ if apiVersion == "" {
353+ return schema.GroupVersion {}, false
354+ }
355+ gv , err := schema .ParseGroupVersion (apiVersion )
356+ if err != nil {
357+ return schema.GroupVersion {}, false
358+ }
359+ return gv , true
360+ }
361+
362+ func findResourceInfoInList (input string , gv schema.GroupVersion , apiResources []metav1.APIResource ) (resourceInfo , bool ) {
363+ group := strings .ToLower (gv .Group )
364+ for _ , apiResource := range apiResources {
365+ if strings .Contains (apiResource .Name , "/" ) {
366+ continue
367+ }
368+ if ! resourceMatchesInput (input , group , apiResource ) {
369+ continue
370+ }
371+ return resourceInfo {
372+ Kind : apiResource .Kind ,
373+ Resource : apiResource .Name ,
374+ Group : gv .Group ,
375+ Version : gv .Version ,
376+ ClusterScoped : ! apiResource .Namespaced ,
377+ }, true
378+ }
379+ return resourceInfo {}, false
380+ }
381+
382+ func resourceMatchesInput (input , group string , apiResource metav1.APIResource ) bool {
383+ candidates := make ([]string , 0 , 3 + len (apiResource .ShortNames ))
384+ if kind := strings .ToLower (strings .TrimSpace (apiResource .Kind )); kind != "" {
385+ candidates = append (candidates , kind )
386+ }
387+ if name := strings .ToLower (strings .TrimSpace (apiResource .Name )); name != "" {
388+ candidates = append (candidates , name )
389+ }
390+ if singular := strings .ToLower (strings .TrimSpace (apiResource .SingularName )); singular != "" {
391+ candidates = append (candidates , singular )
392+ }
393+ for _ , shortName := range apiResource .ShortNames {
394+ if shortName = strings .ToLower (strings .TrimSpace (shortName )); shortName != "" {
395+ candidates = append (candidates , shortName )
396+ }
397+ }
398+
399+ for _ , candidate := range candidates {
400+ if input == candidate {
401+ return true
402+ }
403+ if ! strings .HasSuffix (candidate , "s" ) && input == candidate + "s" {
404+ return true
405+ }
406+ if group != "" && input == candidate + "." + group {
407+ return true
408+ }
409+ if group != "" && ! strings .HasSuffix (candidate , "s" ) && input == candidate + "s." + group {
410+ return true
411+ }
412+ }
413+ return false
414+ }
415+
287416func (r resourceInfo ) GVK () schema.GroupVersionKind {
288417 return schema.GroupVersionKind {Group : r .Group , Version : r .Version , Kind : r .Kind }
289418}
@@ -299,8 +428,7 @@ func normalizeNamespace(r resourceInfo, namespace string) string {
299428 return namespace
300429}
301430
302- func buildObjectForKind (kind string ) * unstructured.Unstructured {
303- resource := resolveResourceInfo (kind )
431+ func buildObjectForResource (resource resourceInfo ) * unstructured.Unstructured {
304432 obj := & unstructured.Unstructured {}
305433 obj .SetGroupVersionKind (resource .GVK ())
306434 return obj
@@ -356,15 +484,15 @@ func permissionNamespace(resource resourceInfo, namespace string) string {
356484 return namespace
357485}
358486
359- func requiredToolPermissions (toolName string , args map [string ]interface {}) ([]toolPermission , error ) {
487+ func requiredToolPermissions (ctx context. Context , cs * cluster. ClientSet , toolName string , args map [string ]interface {}) ([]toolPermission , error ) {
360488 switch toolName {
361489 case "get_resource" :
362490 kind , err := getRequiredString (args , "kind" )
363491 if err != nil {
364492 return nil , err
365493 }
366494 namespace , _ := args ["namespace" ].(string )
367- resource := resolveResourceInfo (kind )
495+ resource := resolveResourceInfo (ctx , cs , kind )
368496 return []toolPermission {{
369497 Resource : resource .Resource ,
370498 Verb : string (common .VerbGet ),
@@ -376,7 +504,7 @@ func requiredToolPermissions(toolName string, args map[string]interface{}) ([]to
376504 return nil , err
377505 }
378506 namespace , _ := args ["namespace" ].(string )
379- resource := resolveResourceInfo (kind )
507+ resource := resolveResourceInfo (ctx , cs , kind )
380508 return []toolPermission {{
381509 Resource : resource .Resource ,
382510 Verb : string (common .VerbGet ),
@@ -407,7 +535,7 @@ func requiredToolPermissions(toolName string, args map[string]interface{}) ([]to
407535 if err != nil {
408536 return nil , err
409537 }
410- resource := resolveResourceInfo ( obj . GetKind () )
538+ resource := resolveResourceInfoForObject ( ctx , cs , obj )
411539 return []toolPermission {{
412540 Resource : resource .Resource ,
413541 Verb : string (common .VerbCreate ),
@@ -418,7 +546,7 @@ func requiredToolPermissions(toolName string, args map[string]interface{}) ([]to
418546 if err != nil {
419547 return nil , err
420548 }
421- resource := resolveResourceInfo ( obj . GetKind () )
549+ resource := resolveResourceInfoForObject ( ctx , cs , obj )
422550 return []toolPermission {{
423551 Resource : resource .Resource ,
424552 Verb : string (common .VerbUpdate ),
@@ -433,7 +561,7 @@ func requiredToolPermissions(toolName string, args map[string]interface{}) ([]to
433561 return nil , err
434562 }
435563 namespace , _ := args ["namespace" ].(string )
436- resource := resolveResourceInfo (kind )
564+ resource := resolveResourceInfo (ctx , cs , kind )
437565 return []toolPermission {{
438566 Resource : resource .Resource ,
439567 Verb : string (common .VerbUpdate ),
@@ -448,7 +576,7 @@ func requiredToolPermissions(toolName string, args map[string]interface{}) ([]to
448576 return nil , err
449577 }
450578 namespace , _ := args ["namespace" ].(string )
451- resource := resolveResourceInfo (kind )
579+ resource := resolveResourceInfo (ctx , cs , kind )
452580 return []toolPermission {{
453581 Resource : resource .Resource ,
454582 Verb : string (common .VerbDelete ),
@@ -480,7 +608,7 @@ func AuthorizeTool(c *gin.Context, cs *cluster.ClientSet, toolName string, args
480608 return "Error: authenticated user not found in context" , true
481609 }
482610
483- permissions , err := requiredToolPermissions (toolName , args )
611+ permissions , err := requiredToolPermissions (c . Request . Context (), cs , toolName , args )
484612 if err != nil {
485613 return "Error: " + err .Error (), true
486614 }
@@ -533,8 +661,8 @@ func executeGetResource(ctx context.Context, cs *cluster.ClientSet, args map[str
533661 }
534662 namespace , _ := args ["namespace" ].(string )
535663
536- resource := resolveResourceInfo (kind )
537- obj := buildObjectForKind ( kind )
664+ resource := resolveResourceInfo (ctx , cs , kind )
665+ obj := buildObjectForResource ( resource )
538666 key := k8stypes.NamespacedName {
539667 Name : name ,
540668 Namespace : normalizeNamespace (resource , namespace ),
@@ -596,7 +724,7 @@ func executeListResources(ctx context.Context, cs *cluster.ClientSet, args map[s
596724 namespace , _ := args ["namespace" ].(string )
597725 labelSelector , _ := args ["label_selector" ].(string )
598726
599- resource := resolveResourceInfo (kind )
727+ resource := resolveResourceInfo (ctx , cs , kind )
600728 namespace = normalizeNamespace (resource , namespace )
601729 list := & unstructured.UnstructuredList {}
602730 list .SetGroupVersionKind (resource .ListGVK ())
@@ -1072,8 +1200,8 @@ func executePatchResource(ctx context.Context, cs *cluster.ClientSet, args map[s
10721200 return "Error: patch must be valid JSON" , true
10731201 }
10741202
1075- resource := resolveResourceInfo (kind )
1076- obj := buildObjectForKind ( kind )
1203+ resource := resolveResourceInfo (ctx , cs , kind )
1204+ obj := buildObjectForResource ( resource )
10771205
10781206 key := k8stypes.NamespacedName {
10791207 Name : name ,
@@ -1104,8 +1232,8 @@ func executeDeleteResource(ctx context.Context, cs *cluster.ClientSet, args map[
11041232 }
11051233 namespace , _ := args ["namespace" ].(string )
11061234
1107- resource := resolveResourceInfo (kind )
1108- obj := buildObjectForKind ( kind )
1235+ resource := resolveResourceInfo (ctx , cs , kind )
1236+ obj := buildObjectForResource ( resource )
11091237
11101238 key := k8stypes.NamespacedName {
11111239 Name : name ,
0 commit comments