@@ -19,12 +19,15 @@ package discovery
19
19
import (
20
20
"context"
21
21
"fmt"
22
+ "slices"
22
23
"strings"
23
24
24
25
"github.com/kcp-dev/kcp/pkg/crdpuller"
25
26
26
27
"k8s.io/apiextensions-apiserver/pkg/apihelpers"
27
28
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"
28
31
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29
32
"k8s.io/apimachinery/pkg/runtime/schema"
30
33
utilerrors "k8s.io/apimachinery/pkg/util/errors"
@@ -33,10 +36,12 @@ import (
33
36
"k8s.io/client-go/discovery"
34
37
"k8s.io/client-go/rest"
35
38
"k8s.io/kube-openapi/pkg/util/proto"
39
+ "k8s.io/utils/ptr"
36
40
)
37
41
38
42
type Client struct {
39
43
discoveryClient discovery.DiscoveryInterface
44
+ crdClient apiextensionsv1client.ApiextensionsV1Interface
40
45
}
41
46
42
47
func NewClient (config * rest.Config ) (* Client , error ) {
@@ -45,40 +50,24 @@ func NewClient(config *rest.Config) (*Client, error) {
45
50
return nil , err
46
51
}
47
52
53
+ crdClient , err := apiextensionsv1client .NewForConfig (config )
54
+ if err != nil {
55
+ return nil , err
56
+ }
57
+
48
58
return & Client {
49
59
discoveryClient : discoveryClient ,
60
+ crdClient : crdClient ,
50
61
}, nil
51
62
}
52
63
53
64
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
65
// 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.
62
67
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.
82
71
83
72
_ , resourceLists , err := c .discoveryClient .ServerGroupsAndResources ()
84
73
if err != nil {
@@ -103,6 +92,96 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
103
92
return nil , fmt .Errorf ("could not find %v in APIs" , gvk )
104
93
}
105
94
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
+
106
185
hasSubResource := func (subResource string ) bool {
107
186
return allResourceNames .Has (resource .Name + "/" + subResource )
108
187
}
@@ -125,13 +204,13 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
125
204
scope = apiextensionsv1 .NamespaceScoped
126
205
}
127
206
128
- crd := & apiextensionsv1.CustomResourceDefinition {
207
+ out := & apiextensionsv1.CustomResourceDefinition {
129
208
TypeMeta : metav1.TypeMeta {
130
209
Kind : "CustomResourceDefinition" ,
131
- APIVersion : "apiextensions.k8s.io/v1" ,
210
+ APIVersion : apiextensionsv1 . SchemeGroupVersion . Identifier () ,
132
211
},
133
212
ObjectMeta : metav1.ObjectMeta {
134
- Name : fmt . Sprintf ( "%s.%s" , resource . Name , gvk . Group ) ,
213
+ Name : crdName ,
135
214
},
136
215
Spec : apiextensionsv1.CustomResourceDefinitionSpec {
137
216
Group : gvk .Group ,
@@ -160,13 +239,28 @@ func (c *Client) RetrieveCRD(ctx context.Context, gvk schema.GroupVersionKind) (
160
239
},
161
240
}
162
241
163
- apiextensionsv1 .SetDefaults_CustomResourceDefinition (crd )
242
+ apiextensionsv1 .SetDefaults_CustomResourceDefinition (out )
164
243
165
244
if apihelpers .IsProtectedCommunityGroup (gvk .Group ) {
166
- crd .Annotations = map [string ]string {
245
+ out .Annotations = map [string ]string {
167
246
apiextensionsv1 .KubeAPIApprovedAnnotation : "https://github.com/kcp-dev/kubernetes/pull/4" ,
168
247
}
169
248
}
170
249
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
172
266
}
0 commit comments