@@ -19,39 +19,154 @@ package discovery
1919import (
2020 "context"
2121 "fmt"
22+ "strings"
2223
24+ "github.com/kcp-dev/kcp/pkg/crdpuller"
25+
26+ "k8s.io/apiextensions-apiserver/pkg/apihelpers"
2327 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2429 "k8s.io/apimachinery/pkg/runtime/schema"
25- ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
30+ utilerrors "k8s.io/apimachinery/pkg/util/errors"
31+ "k8s.io/apimachinery/pkg/util/sets"
32+ "k8s.io/apiserver/pkg/endpoints/openapi"
33+ "k8s.io/client-go/discovery"
34+ "k8s.io/client-go/rest"
35+ "k8s.io/kube-openapi/pkg/util/proto"
2636)
2737
2838type Client struct {
29- kubeClient ctrlruntimeclient. Reader
39+ discoveryClient discovery. DiscoveryInterface
3040}
3141
32- func NewClient (kubeClient ctrlruntimeclient.Client ) * Client {
33- return & Client {
34- kubeClient : kubeClient ,
42+ func NewClient (config * rest.Config ) (* Client , error ) {
43+ discoveryClient , err := discovery .NewDiscoveryClientForConfig (config )
44+ if err != nil {
45+ return nil , err
3546 }
47+
48+ return & Client {
49+ discoveryClient : discoveryClient ,
50+ }, nil
3651}
3752
38- func (c * Client ) DiscoverResourceType (ctx context.Context , gk schema.GroupKind ) (* apiextensionsv1.CustomResourceDefinition , error ) {
39- crds := & apiextensionsv1.CustomResourceDefinitionList {}
40- if err := c .kubeClient .List (ctx , crds ); err != nil {
41- return nil , fmt .Errorf ("failed to list CRDs: %w" , err )
53+ func (c * Client ) RetrieveCRD (ctx context.Context , gvk schema.GroupVersionKind ) (* apiextensionsv1.CustomResourceDefinition , error ) {
54+ openapiSchema , err := c .discoveryClient .OpenAPISchema ()
55+ if err != nil {
56+ return nil , err
57+ }
58+
59+ // Most of this code follows the logic in kcp's crd-puller, but is slimmed down
60+ // to a) only support openapi and b) extract a specific version, not necessarily
61+ // the preferred version.
62+
63+ models , err := proto .NewOpenAPIData (openapiSchema )
64+ if err != nil {
65+ return nil , err
66+ }
67+ modelsByGKV , err := openapi .GetModelsByGKV (models )
68+ if err != nil {
69+ return nil , err
4270 }
4371
44- for _ , crd := range crds .Items {
45- if crd .Spec .Group != gk .Group {
46- continue
72+ protoSchema := modelsByGKV [gvk ]
73+ if protoSchema == nil {
74+ return nil , fmt .Errorf ("no models for %v" , gvk )
75+ }
76+
77+ var schemaProps apiextensionsv1.JSONSchemaProps
78+ errs := crdpuller .Convert (protoSchema , & schemaProps )
79+ if len (errs ) > 0 {
80+ return nil , utilerrors .NewAggregate (errs )
81+ }
82+
83+ _ , resourceLists , err := c .discoveryClient .ServerGroupsAndResources ()
84+ if err != nil {
85+ return nil , err
86+ }
87+
88+ var resource * metav1.APIResource
89+ allResourceNames := sets .New [string ]()
90+ for _ , resList := range resourceLists {
91+ for _ , res := range resList .APIResources {
92+ allResourceNames .Insert (res .Name )
93+
94+ // find the requested resource based on the Kind, but ensure that subresources
95+ // are not misinterpreted as the main resource by checking for "/"
96+ if resList .GroupVersion == gvk .GroupVersion ().String () && res .Kind == gvk .Kind && ! strings .Contains (res .Name , "/" ) {
97+ resource = & res
98+ }
4799 }
100+ }
101+
102+ if resource == nil {
103+ return nil , fmt .Errorf ("could not find %v in APIs" , gvk )
104+ }
105+
106+ hasSubResource := func (subResource string ) bool {
107+ return allResourceNames .Has (resource .Name + "/" + subResource )
108+ }
109+
110+ var statusSubResource * apiextensionsv1.CustomResourceSubresourceStatus
111+ if hasSubResource ("status" ) {
112+ statusSubResource = & apiextensionsv1.CustomResourceSubresourceStatus {}
113+ }
48114
49- if crd .Spec .Names .Kind != gk .Kind {
50- continue
115+ var scaleSubResource * apiextensionsv1.CustomResourceSubresourceScale
116+ if hasSubResource ("scale" ) {
117+ scaleSubResource = & apiextensionsv1.CustomResourceSubresourceScale {
118+ SpecReplicasPath : ".spec.replicas" ,
119+ StatusReplicasPath : ".status.replicas" ,
51120 }
121+ }
122+
123+ scope := apiextensionsv1 .ClusterScoped
124+ if resource .Namespaced {
125+ scope = apiextensionsv1 .NamespaceScoped
126+ }
52127
53- return & crd , nil
128+ crd := & apiextensionsv1.CustomResourceDefinition {
129+ TypeMeta : metav1.TypeMeta {
130+ Kind : "CustomResourceDefinition" ,
131+ APIVersion : "apiextensions.k8s.io/v1" ,
132+ },
133+ ObjectMeta : metav1.ObjectMeta {
134+ Name : fmt .Sprintf ("%s.%s" , resource .Name , gvk .Group ),
135+ },
136+ Spec : apiextensionsv1.CustomResourceDefinitionSpec {
137+ Group : gvk .Group ,
138+ Versions : []apiextensionsv1.CustomResourceDefinitionVersion {
139+ {
140+ Name : gvk .Version ,
141+ Schema : & apiextensionsv1.CustomResourceValidation {
142+ OpenAPIV3Schema : & schemaProps ,
143+ },
144+ Subresources : & apiextensionsv1.CustomResourceSubresources {
145+ Status : statusSubResource ,
146+ Scale : scaleSubResource ,
147+ },
148+ Served : true ,
149+ Storage : true ,
150+ },
151+ },
152+ Scope : scope ,
153+ Names : apiextensionsv1.CustomResourceDefinitionNames {
154+ Plural : resource .Name ,
155+ Kind : resource .Kind ,
156+ Categories : resource .Categories ,
157+ ShortNames : resource .ShortNames ,
158+ Singular : resource .SingularName ,
159+ },
160+ },
161+ }
162+
163+ apiextensionsv1 .SetDefaults_CustomResourceDefinition (crd )
164+
165+ if apihelpers .IsProtectedCommunityGroup (gvk .Group ) {
166+ crd .Annotations = map [string ]string {
167+ apiextensionsv1 .KubeAPIApprovedAnnotation : "https://github.com/kcp-dev/kubernetes/pull/4" ,
168+ }
54169 }
55170
56- return nil , fmt . Errorf ( "CustomResourceDefinition for %s/%s does not exist" , gk . Group , gk . Kind )
171+ return crd , nil
57172}
0 commit comments