@@ -18,6 +18,7 @@ package discovery
1818
1919import (
2020 "context"
21+ "errors"
2122 "fmt"
2223 "slices"
2324 "strings"
@@ -32,6 +33,7 @@ import (
3233 "k8s.io/apimachinery/pkg/runtime/schema"
3334 utilerrors "k8s.io/apimachinery/pkg/util/errors"
3435 "k8s.io/apimachinery/pkg/util/sets"
36+ "k8s.io/apimachinery/pkg/version"
3537 "k8s.io/apiserver/pkg/endpoints/openapi"
3638 "k8s.io/client-go/discovery"
3739 "k8s.io/client-go/rest"
@@ -61,47 +63,67 @@ func NewClient(config *rest.Config) (*Client, error) {
6163 }, nil
6264}
6365
64- func (c * Client ) RetrieveCRD (ctx context.Context , gvk schema.GroupVersionKind ) (* apiextensionsv1.CustomResourceDefinition , error ) {
65- // Most of this code follows the logic in kcp's crd-puller, but is slimmed down
66- // to extract a specific version, not necessarily the preferred version.
67-
66+ func (c * Client ) RetrieveCRD (ctx context.Context , gk schema.GroupKind ) (* apiextensionsv1.CustomResourceDefinition , error ) {
6867 ////////////////////////////////////
69- // Resolve GVK into GVR , because we need the resource name to construct
68+ // Resolve GK into GR , because we need the resource name to construct
7069 // the full CRD name.
7170
7271 _ , resourceLists , err := c .discoveryClient .ServerGroupsAndResources ()
7372 if err != nil {
7473 return nil , err
7574 }
7675
76+ // resource is the resource described by gk in any of the found versions
7777 var resource * metav1.APIResource
78- allResourceNames := sets .New [string ]()
78+
79+ availableVersions := sets .New [string ]()
80+ subresourcesPerVersion := map [string ]sets.Set [string ]{}
81+
7982 for _ , resList := range resourceLists {
8083 for _ , res := range resList .APIResources {
81- allResourceNames .Insert (res .Name )
84+ // ignore other groups
85+ if res .Group != gk .Group || res .Kind != gk .Kind {
86+ continue
87+ }
8288
83- // find the requested resource based on the Kind, but ensure that subresources
84- // are not misinterpreted as the main resource by checking for "/"
85- if resList .GroupVersion == gvk .GroupVersion ().String () && res .Kind == gvk .Kind && ! strings .Contains (res .Name , "/" ) {
89+ // res could describe the main resource or one of its subresources.
90+ name := res .Name
91+ subresource := ""
92+ if strings .Contains (name , "/" ) {
93+ parts := strings .SplitN (name , "/" , 2 )
94+ name = parts [0 ]
95+ subresource = parts [1 ]
96+ }
97+
98+ if subresource == "" {
8699 resource = & res
100+ } else {
101+ list , ok := subresourcesPerVersion [res .Version ]
102+ if ! ok {
103+ list = sets .New [string ]()
104+ }
105+ list .Insert (subresource )
106+ subresourcesPerVersion [res .Version ] = list
87107 }
108+
109+ availableVersions .Insert (res .Version )
88110 }
89111 }
90112
91113 if resource == nil {
92- return nil , fmt .Errorf ("could not find %v in APIs" , gvk )
114+ return nil , fmt .Errorf ("could not find %v in APIs" , gk )
93115 }
94116
95117 ////////////////////////////////////
96- // If possible, retrieve the GVK as its original CRD, which is always preferred
118+ // If possible, retrieve the GK as its original CRD, which is always preferred
97119 // because it's much more precise than what we can retrieve from the OpenAPI.
98120 // If no CRD can be found, fallback to the OpenAPI schema.
99121
100122 crdName := resource .Name
101- if gvk .Group == "" {
123+ if gk .Group == "" {
102124 crdName += ".core"
103125 } else {
104- crdName += "." + gvk .Group
126+ crdName += "." + gk .Group
105127 }
106128
107129 crd , err := c .crdClient .CustomResourceDefinitions ().Get (ctx , crdName , metav1.GetOptions {})
@@ -110,25 +132,20 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
110132 // of re-creating it later on based on the openapi schema, we take the original
111133 // CRD and just strip it down to what we need.
112134 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-
125135 if apihelpers .IsCRDConditionTrue (crd , apiextensionsv1 .NonStructuralSchema ) {
126- crd . Spec . Versions [ 0 ]. Schema = & apiextensionsv1.CustomResourceValidation {
136+ emptySchema : = & apiextensionsv1.CustomResourceValidation {
127137 OpenAPIV3Schema : & apiextensionsv1.JSONSchemaProps {
128138 Type : "object" ,
129139 XPreserveUnknownFields : ptr .To (true ),
130140 },
131141 }
142+
143+ for i , version := range crd .Spec .Versions {
144+ if version .Schema == nil || version .Schema .OpenAPIV3Schema == nil {
145+ version .Schema = emptySchema
146+ crd .Spec .Versions [i ] = version
147+ }
148+ }
132149 }
133150
134151 crd .APIVersion = apiextensionsv1 .SchemeGroupVersion .Identifier ()
@@ -140,6 +157,7 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
140157 Name : oldMeta .Name ,
141158 Annotations : filterAnnotations (oldMeta .Annotations ),
142159 }
160+ crd .Status .Conditions = []apiextensionsv1.CustomResourceDefinitionCondition {}
143161
144162 // There is only ever one version, so conversion rules do not make sense
145163 // (and even if they did, the conversion webhook from the service cluster
@@ -156,49 +174,34 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
156174 return nil , err
157175 }
158176
177+ ////////////////////////////////////
159178 // CRD not found, so fall back to using the OpenAPI schema
179+
160180 openapiSchema , err := c .discoveryClient .OpenAPISchema ()
161181 if err != nil {
162182 return nil , err
163183 }
164184
165- models , err := proto .NewOpenAPIData (openapiSchema )
166- if err != nil {
167- return nil , err
168- }
169- modelsByGKV , err := openapi .GetModelsByGKV (models )
185+ preferredVersion , err := c .getPreferredVersion (resource )
170186 if err != nil {
171187 return nil , err
172188 }
173189
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 )
190+ if preferredVersion == "" {
191+ return nil , errors .New ("cannot determine storage version because no preferred version exists in the schema" )
183192 }
184193
185- hasSubResource := func (subResource string ) bool {
186- return allResourceNames .Has (resource .Name + "/" + subResource )
187- }
188-
189- var statusSubResource * apiextensionsv1.CustomResourceSubresourceStatus
190- if hasSubResource ("status" ) {
191- statusSubResource = & apiextensionsv1.CustomResourceSubresourceStatus {}
194+ models , err := proto .NewOpenAPIData (openapiSchema )
195+ if err != nil {
196+ return nil , err
192197 }
193198
194- var scaleSubResource * apiextensionsv1.CustomResourceSubresourceScale
195- if hasSubResource ("scale" ) {
196- scaleSubResource = & apiextensionsv1.CustomResourceSubresourceScale {
197- SpecReplicasPath : ".spec.replicas" ,
198- StatusReplicasPath : ".status.replicas" ,
199- }
199+ modelsByGKV , err := openapi .GetModelsByGKV (models )
200+ if err != nil {
201+ return nil , err
200202 }
201203
204+ // prepare an empty CRD
202205 scope := apiextensionsv1 .ClusterScoped
203206 if resource .Namespaced {
204207 scope = apiextensionsv1 .NamespaceScoped
@@ -213,22 +216,9 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
213216 Name : crdName ,
214217 },
215218 Spec : apiextensionsv1.CustomResourceDefinitionSpec {
216- Group : gvk .Group ,
217- Versions : []apiextensionsv1.CustomResourceDefinitionVersion {
218- {
219- Name : gvk .Version ,
220- Schema : & apiextensionsv1.CustomResourceValidation {
221- OpenAPIV3Schema : & schemaProps ,
222- },
223- Subresources : & apiextensionsv1.CustomResourceSubresources {
224- Status : statusSubResource ,
225- Scale : scaleSubResource ,
226- },
227- Served : true ,
228- Storage : true ,
229- },
230- },
231- Scope : scope ,
219+ Group : gk .Group ,
220+ Versions : []apiextensionsv1.CustomResourceDefinitionVersion {},
221+ Scope : scope ,
232222 Names : apiextensionsv1.CustomResourceDefinitionNames {
233223 Plural : resource .Name ,
234224 Kind : resource .Kind ,
@@ -239,9 +229,62 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
239229 },
240230 }
241231
232+ // fill-in the schema for each version, making sure that versions are sorted
233+ // according to Kubernetes rules.
234+ sortedVersions := availableVersions .UnsortedList ()
235+ slices .SortFunc (sortedVersions , func (a , b string ) int {
236+ return version .CompareKubeAwareVersionStrings (a , b )
237+ })
238+
239+ for _ , version := range sortedVersions {
240+ subresources := subresourcesPerVersion [version ]
241+ gvk := schema.GroupVersionKind {
242+ Group : gk .Group ,
243+ Version : version ,
244+ Kind : gk .Kind ,
245+ }
246+
247+ protoSchema := modelsByGKV [gvk ]
248+ if protoSchema == nil {
249+ return nil , fmt .Errorf ("no models for %v" , gk )
250+ }
251+
252+ var schemaProps apiextensionsv1.JSONSchemaProps
253+ errs := crdpuller .Convert (protoSchema , & schemaProps )
254+ if len (errs ) > 0 {
255+ return nil , utilerrors .NewAggregate (errs )
256+ }
257+
258+ var statusSubResource * apiextensionsv1.CustomResourceSubresourceStatus
259+ if subresources .Has ("status" ) {
260+ statusSubResource = & apiextensionsv1.CustomResourceSubresourceStatus {}
261+ }
262+
263+ var scaleSubResource * apiextensionsv1.CustomResourceSubresourceScale
264+ if subresources .Has ("scale" ) {
265+ scaleSubResource = & apiextensionsv1.CustomResourceSubresourceScale {
266+ SpecReplicasPath : ".spec.replicas" ,
267+ StatusReplicasPath : ".status.replicas" ,
268+ }
269+ }
270+
271+ out .Spec .Versions = append (out .Spec .Versions , apiextensionsv1.CustomResourceDefinitionVersion {
272+ Name : version ,
273+ Schema : & apiextensionsv1.CustomResourceValidation {
274+ OpenAPIV3Schema : & schemaProps ,
275+ },
276+ Subresources : & apiextensionsv1.CustomResourceSubresources {
277+ Status : statusSubResource ,
278+ Scale : scaleSubResource ,
279+ },
280+ Served : true ,
281+ Storage : version == preferredVersion ,
282+ })
283+ }
284+
242285 apiextensionsv1 .SetDefaults_CustomResourceDefinition (out )
243286
244- if apihelpers .IsProtectedCommunityGroup (gvk .Group ) {
287+ if apihelpers .IsProtectedCommunityGroup (gk .Group ) {
245288 out .Annotations = map [string ]string {
246289 apiextensionsv1 .KubeAPIApprovedAnnotation : "https://github.com/kcp-dev/kubernetes/pull/4" ,
247290 }
@@ -250,6 +293,23 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
250293 return out , nil
251294}
252295
296+ func (c * Client ) getPreferredVersion (resource * metav1.APIResource ) (string , error ) {
297+ result , err := c .discoveryClient .ServerPreferredResources ()
298+ if err != nil {
299+ return "" , err
300+ }
301+
302+ for _ , resList := range result {
303+ for _ , res := range resList .APIResources {
304+ if res .Name == resource .Name && res .Group == resource .Group {
305+ return res .Version , nil
306+ }
307+ }
308+ }
309+
310+ return "" , nil
311+ }
312+
253313func filterAnnotations (ann map [string ]string ) map [string ]string {
254314 allowlist := []string {
255315 apiextensionsv1 .KubeAPIApprovedAnnotation ,
0 commit comments