diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go index 933849a6dba..5f4140b1238 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/main.go @@ -43,7 +43,10 @@ func init() { // RegisterWithGRPCServer enables a plugin to register with a gRPC server // returning the server implementation. func RegisterWithGRPCServer(s grpc.ServiceRegistrar, configGetter core.KubernetesConfigGetter, clustersConfig kube.ClustersConfig, pluginConfigPath string) (interface{}, error) { - svr := NewServer(configGetter) + svr, err := NewServer(configGetter) + if err != nil { + return nil, err + } v1alpha1.RegisterResourcesServiceServer(s, svr) return svr, nil } diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go index 2122c93166f..343f88cdb46 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/server.go @@ -26,11 +26,16 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" log "k8s.io/klog/v2" + "github.com/kubeapps/kubeapps/cmd/apprepository-controller/pkg/client/clientset/versioned/scheme" "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/core" pkgsGRPCv1alpha1 "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/core/packages/v1alpha1" "github.com/kubeapps/kubeapps/cmd/kubeapps-apis/gen/plugins/resources/v1alpha1" @@ -49,9 +54,45 @@ type Server struct { // corePackagesClientGetter holds a function to obtain the core.packages.v1alpha1 // client. It is similarly initialised in NewServer() below. corePackagesClientGetter func() (pkgsGRPCv1alpha1.PackagesServiceClient, error) + + // We keep a restmapper to cache discovery of REST mappings from GVK->GVR. + restMapper meta.RESTMapper + + // kindToResource is a function to convert a GVK to GVR. Can be replaced + // in tests with a dummy version using the unsafe helpers while the real + // implementation queries the k8s API for a REST mapper. + kindToResource func(meta.RESTMapper, schema.GroupVersionKind) (schema.GroupVersionResource, error) +} + +// createRESTMapper returns a rest mapper configured with the APIs of the +// local k8s API server. This is used to convert between the GroupVersionKinds +// of the resource references to the GroupVersionResource used by the API server. +func createRESTMapper() (meta.RESTMapper, error) { + config, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + // To use the config with RESTClientFor, extra fields are required. + // See https://github.com/kubernetes/client-go/issues/657#issuecomment-842960258 + config.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} + config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: scheme.Codecs} + client, err := rest.RESTClientFor(config) + if err != nil { + return nil, err + } + discoveryClient := discovery.NewDiscoveryClient(client) + groupResources, err := restmapper.GetAPIGroupResources(discoveryClient) + if err != nil { + return nil, err + } + return restmapper.NewDiscoveryRESTMapper(groupResources), nil } -func NewServer(configGetter core.KubernetesConfigGetter) *Server { +func NewServer(configGetter core.KubernetesConfigGetter) (*Server, error) { + mapper, err := createRESTMapper() + if err != nil { + return nil, err + } return &Server{ clientGetter: func(ctx context.Context, cluster string) (kubernetes.Interface, dynamic.Interface, error) { if configGetter == nil { @@ -79,7 +120,15 @@ func NewServer(configGetter core.KubernetesConfigGetter) *Server { } return pkgsGRPCv1alpha1.NewPackagesServiceClient(conn), nil }, - } + restMapper: mapper, + kindToResource: func(mapper meta.RESTMapper, gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + mapping, err := mapper.RESTMapping(gvk.GroupKind()) + if err != nil { + return schema.GroupVersionResource{}, err + } + return mapping.Resource, nil + }, + }, nil } // GetResources returns the resources for an installed package. @@ -142,10 +191,11 @@ func (s *Server) GetResources(r *v1alpha1.GetResourcesRequest, stream v1alpha1.R return status.Errorf(codes.Internal, "unable to parse group version from %q: %s", ref.ApiVersion, err.Error()) } gvk := groupVersion.WithKind(ref.Kind) - // TODO(minelson): Find alternative to UnsafeGuessKindToResource. - // Looks to involve restmapper.NewDeferredDiscoveryRESTMapper(discovery.NewDiscoveryClient(c)) - // See https://pkg.go.dev/k8s.io/client-go/restmapper#NewDeferredDiscoveryRESTMapper - gvr, _ := meta.UnsafeGuessKindToResource(gvk) + + gvr, err := s.kindToResource(s.restMapper, gvk) + if err != nil { + return status.Errorf(codes.Internal, "unable to map group-kind %v to resource: %s", gvk.GroupKind(), err.Error()) + } if !r.GetWatch() { resource, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(stream.Context(), ref.GetName(), metav1.GetOptions{}) diff --git a/cmd/kubeapps-apis/plugins/resources/v1alpha1/server_test.go b/cmd/kubeapps-apis/plugins/resources/v1alpha1/server_test.go index 9d2d8a0d42c..6c063a44998 100644 --- a/cmd/kubeapps-apis/plugins/resources/v1alpha1/server_test.go +++ b/cmd/kubeapps-apis/plugins/resources/v1alpha1/server_test.go @@ -31,8 +31,10 @@ import ( "google.golang.org/grpc/test/bufconn" apps "k8s.io/api/apps/v1" core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" dynfake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes" @@ -105,6 +107,12 @@ func getResourcesClient(t *testing.T, objects ...runtime.Object) (v1alpha1.Resou corePackagesClientGetter: func() (pkgsGRPCv1alpha1.PackagesServiceClient, error) { return pkgsGRPCv1alpha1.NewPackagesServiceClient(conn), nil }, + // For testing, define a kindToResource converter that doesn't require + // a rest mapper. + kindToResource: func(mapper meta.RESTMapper, gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + gvr, _ := meta.UnsafeGuessKindToResource(gvk) + return gvr, nil + }, }) go func() {