Skip to content

Commit 488f837

Browse files
committed
prefer CRDs when discovery API types, as CRDs contain more betterer information
On-behalf-of: @SAP [email protected]
1 parent 5d43eb0 commit 488f837

File tree

1 file changed

+126
-32
lines changed

1 file changed

+126
-32
lines changed

internal/discovery/client.go

+126-32
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ package discovery
1919
import (
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

3842
type Client struct {
3943
discoveryClient discovery.DiscoveryInterface
44+
crdClient apiextensionsv1client.ApiextensionsV1Interface
4045
}
4146

4247
func 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

5364
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-
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

Comments
 (0)