@@ -19,12 +19,15 @@ package discovery
1919import (
2020 "context"
2121 "fmt"
22+ "slices"
2223 "strings"
2324
2425 "github.com/kcp-dev/kcp/pkg/crdpuller"
2526
2627 "k8s.io/apiextensions-apiserver/pkg/apihelpers"
2728 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29+ apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
30+ apierrors "k8s.io/apimachinery/pkg/api/errors"
2831 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2932 "k8s.io/apimachinery/pkg/runtime/schema"
3033 utilerrors "k8s.io/apimachinery/pkg/util/errors"
@@ -33,10 +36,12 @@ import (
3336 "k8s.io/client-go/discovery"
3437 "k8s.io/client-go/rest"
3538 "k8s.io/kube-openapi/pkg/util/proto"
39+ "k8s.io/utils/ptr"
3640)
3741
3842type Client struct {
3943 discoveryClient discovery.DiscoveryInterface
44+ crdClient apiextensionsv1client.ApiextensionsV1Interface
4045}
4146
4247func NewClient (config * rest.Config ) (* Client , error ) {
@@ -45,40 +50,24 @@ func NewClient(config *rest.Config) (*Client, error) {
4550 return nil , err
4651 }
4752
53+ crdClient , err := apiextensionsv1client .NewForConfig (config )
54+ if err != nil {
55+ return nil , err
56+ }
57+
4858 return & Client {
4959 discoveryClient : discoveryClient ,
60+ crdClient : crdClient ,
5061 }, nil
5162}
5263
5364func (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-
5965 // 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.
66+ // to extract a specific version, not necessarily the preferred version.
6267
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
70- }
71-
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- }
68+ ////////////////////////////////////
69+ // Resolve GVK into GVR, because we need the resource name to construct
70+ // the full CRD name.
8271
8372 _ , resourceLists , err := c .discoveryClient .ServerGroupsAndResources ()
8473 if err != nil {
@@ -103,6 +92,96 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
10392 return nil , fmt .Errorf ("could not find %v in APIs" , gvk )
10493 }
10594
95+ ////////////////////////////////////
96+ // If possible, retrieve the GVK as its original CRD, which is always preferred
97+ // because it's much more precise than what we can retrieve from the OpenAPI.
98+ // If no CRD can be found, fallback to the OpenAPI schema.
99+
100+ crdName := resource .Name
101+ if gvk .Group == "" {
102+ crdName += ".core"
103+ } else {
104+ crdName += "." + gvk .Group
105+ }
106+
107+ crd , err := c .crdClient .CustomResourceDefinitions ().Get (ctx , crdName , metav1.GetOptions {})
108+
109+ // Hooray, we found a CRD! There is so much goodness on a real CRD that instead
110+ // of re-creating it later on based on the openapi schema, we take the original
111+ // CRD and just strip it down to what we need.
112+ if err == nil {
113+ // remove all but the requested version
114+ crd .Spec .Versions = slices .DeleteFunc (crd .Spec .Versions , func (ver apiextensionsv1.CustomResourceDefinitionVersion ) bool {
115+ return ver .Name != gvk .Version
116+ })
117+
118+ if len (crd .Spec .Versions ) == 0 {
119+ return nil , fmt .Errorf ("CRD %s does not contain version %s" , crdName , gvk .Version )
120+ }
121+
122+ crd .Spec .Versions [0 ].Served = true
123+ crd .Spec .Versions [0 ].Storage = true
124+
125+ if apihelpers .IsCRDConditionTrue (crd , apiextensionsv1 .NonStructuralSchema ) {
126+ crd .Spec .Versions [0 ].Schema = & apiextensionsv1.CustomResourceValidation {
127+ OpenAPIV3Schema : & apiextensionsv1.JSONSchemaProps {
128+ Type : "object" ,
129+ XPreserveUnknownFields : ptr .To (true ),
130+ },
131+ }
132+ }
133+
134+ crd .APIVersion = apiextensionsv1 .SchemeGroupVersion .Identifier ()
135+ crd .Kind = "CustomResourceDefinition"
136+
137+ // cleanup object meta
138+ oldMeta := crd .ObjectMeta
139+ crd .ObjectMeta = metav1.ObjectMeta {
140+ Name : oldMeta .Name ,
141+ Annotations : filterAnnotations (oldMeta .Annotations ),
142+ }
143+
144+ // There is only ever one version, so conversion rules do not make sense
145+ // (and even if they did, the conversion webhook from the service cluster
146+ // would not be available in kcp anyway).
147+ crd .Spec .Conversion = & apiextensionsv1.CustomResourceConversion {
148+ Strategy : apiextensionsv1 .NoneConverter ,
149+ }
150+
151+ return crd , nil
152+ }
153+
154+ // any non-404 error is permanent
155+ if ! apierrors .IsNotFound (err ) {
156+ return nil , err
157+ }
158+
159+ // CRD not found, so fall back to using the OpenAPI schema
160+ openapiSchema , err := c .discoveryClient .OpenAPISchema ()
161+ if err != nil {
162+ return nil , err
163+ }
164+
165+ models , err := proto .NewOpenAPIData (openapiSchema )
166+ if err != nil {
167+ return nil , err
168+ }
169+ modelsByGKV , err := openapi .GetModelsByGKV (models )
170+ if err != nil {
171+ return nil , err
172+ }
173+
174+ protoSchema := modelsByGKV [gvk ]
175+ if protoSchema == nil {
176+ return nil , fmt .Errorf ("no models for %v" , gvk )
177+ }
178+
179+ var schemaProps apiextensionsv1.JSONSchemaProps
180+ errs := crdpuller .Convert (protoSchema , & schemaProps )
181+ if len (errs ) > 0 {
182+ return nil , utilerrors .NewAggregate (errs )
183+ }
184+
106185 hasSubResource := func (subResource string ) bool {
107186 return allResourceNames .Has (resource .Name + "/" + subResource )
108187 }
@@ -125,13 +204,13 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
125204 scope = apiextensionsv1 .NamespaceScoped
126205 }
127206
128- crd := & apiextensionsv1.CustomResourceDefinition {
207+ out := & apiextensionsv1.CustomResourceDefinition {
129208 TypeMeta : metav1.TypeMeta {
130209 Kind : "CustomResourceDefinition" ,
131- APIVersion : "apiextensions.k8s.io/v1" ,
210+ APIVersion : apiextensionsv1 . SchemeGroupVersion . Identifier () ,
132211 },
133212 ObjectMeta : metav1.ObjectMeta {
134- Name : fmt . Sprintf ( "%s.%s" , resource . Name , gvk . Group ) ,
213+ Name : crdName ,
135214 },
136215 Spec : apiextensionsv1.CustomResourceDefinitionSpec {
137216 Group : gvk .Group ,
@@ -160,13 +239,28 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
160239 },
161240 }
162241
163- apiextensionsv1 .SetDefaults_CustomResourceDefinition (crd )
242+ apiextensionsv1 .SetDefaults_CustomResourceDefinition (out )
164243
165244 if apihelpers .IsProtectedCommunityGroup (gvk .Group ) {
166- crd .Annotations = map [string ]string {
245+ out .Annotations = map [string ]string {
167246 apiextensionsv1 .KubeAPIApprovedAnnotation : "https://github.com/kcp-dev/kubernetes/pull/4" ,
168247 }
169248 }
170249
171- return crd , nil
250+ return out , nil
251+ }
252+
253+ func filterAnnotations (ann map [string ]string ) map [string ]string {
254+ allowlist := []string {
255+ apiextensionsv1 .KubeAPIApprovedAnnotation ,
256+ }
257+
258+ out := map [string ]string {}
259+ for k , v := range ann {
260+ if slices .Contains (allowlist , k ) {
261+ out [k ] = v
262+ }
263+ }
264+
265+ return out
172266}
0 commit comments