Skip to content

Commit b607c71

Browse files
committed
fix bug preventing the creation of CRD in virtual workspace
Signed-off-by: olalekan odukoya <odukoyaonline@gmail.com>
1 parent c749f92 commit b607c71

8 files changed

Lines changed: 447 additions & 6 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
Copyright 2026 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package apiserver
18+
19+
import (
20+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
21+
22+
apisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
23+
)
24+
25+
// isCRDResource checks if a CustomResourceDefinition is for the CRD type itself
26+
// (apiextensions.k8s.io/v1.CustomResourceDefinition).
27+
//
28+
// This is used to trigger special handling for CRD's deeply nested, recursive schema structure.
29+
// Without this, the OpenAPI builder would generate an incomplete schema where nested fields like
30+
// spec.versions[].schema.openAPIV3Schema.properties are truncated to <Object> instead of showing
31+
// their actual types.
32+
//
33+
// When this returns true, ensureCompleteSchemaGeneration() is called to set
34+
// XPreserveUnknownFields=false on the CRD's schema, forcing the OpenAPI builder to generate
35+
// the complete schema tree
36+
//
37+
// See: https://github.com/kcp-dev/kcp/issues/3389
38+
func isCRDResource(crd *apiextensionsv1.CustomResourceDefinition) bool {
39+
return crd.Spec.Group == "apiextensions.k8s.io" && crd.Spec.Names.Kind == "CustomResourceDefinition"
40+
}
41+
42+
func isCRDAPIResourceSchema(apiResourceSchema *apisv1alpha1.APIResourceSchema) bool {
43+
return apiResourceSchema.Spec.Group == "apiextensions.k8s.io" && apiResourceSchema.Spec.Names.Kind == "CustomResourceDefinition"
44+
}

pkg/virtual/framework/dynamic/apiserver/openapi.go

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"net/http"
2525
"sort"
26+
"strings"
2627
"time"
2728

2829
"github.com/emicklei/go-restful/v3"
@@ -42,6 +43,7 @@ import (
4243
"k8s.io/kube-openapi/pkg/common/restfuladapter"
4344
"k8s.io/kube-openapi/pkg/handler3"
4445
"k8s.io/kube-openapi/pkg/spec3"
46+
validationspec "k8s.io/kube-openapi/pkg/validation/spec"
4547
"k8s.io/utils/keymutex"
4648
"k8s.io/utils/lru"
4749

@@ -305,12 +307,20 @@ func apiResourceSchemaToSpec(apiResourceSchema *apisv1alpha1.APIResourceSchema)
305307
},
306308
}
307309

308-
for _, ver := range versions {
310+
if isCRDResource(crd) {
311+
ensureCompleteSchemaGeneration(crd)
312+
}
313+
314+
for i, ver := range versions {
309315
spec, err := builder.BuildOpenAPIV3(crd, ver.Name, builder.Options{V2: false})
310316
if err != nil {
311317
return nil, err
312318
}
313319

320+
if isCRDResource(crd) {
321+
patchCRDSchemaForOpenAPI(spec, &apiResourceSchema.Spec.Versions[i])
322+
}
323+
314324
gv := schema.GroupVersion{Group: crd.Spec.Group, Version: ver.Name}
315325
gvPath := groupVersionToOpenAPIV3Path(gv)
316326
specs[gvPath] = append(specs[gvPath], spec)
@@ -319,6 +329,112 @@ func apiResourceSchemaToSpec(apiResourceSchema *apisv1alpha1.APIResourceSchema)
319329
return specs, nil
320330
}
321331

332+
// This is used to trigger special handling for CRD's recursive schema structure, ensuring
333+
// the OpenAPI builder generates a complete schema instead of a shallow one (due to
334+
// XPreserveUnknownFields behavior) and allowing validation of OpenAPI keywords.
335+
func ensureCompleteSchemaGeneration(crd *apiextensionsv1.CustomResourceDefinition) {
336+
falseVal := false
337+
for i := range crd.Spec.Versions {
338+
if crd.Spec.Versions[i].Schema != nil && crd.Spec.Versions[i].Schema.OpenAPIV3Schema != nil {
339+
crd.Spec.Versions[i].Schema.OpenAPIV3Schema.XPreserveUnknownFields = &falseVal
340+
}
341+
}
342+
}
343+
344+
func patchCRDSchemaForOpenAPI(spec *spec3.OpenAPI, ver *apisv1alpha1.APIResourceVersion) {
345+
if spec.Components == nil || spec.Components.Schemas == nil {
346+
return
347+
}
348+
349+
for name, s := range spec.Components.Schemas {
350+
if !strings.HasSuffix(name, "CustomResourceDefinition") {
351+
continue
352+
}
353+
if _, hasSpec := s.Properties["spec"]; hasSpec {
354+
continue
355+
}
356+
357+
sourceSchema, err := ver.GetSchema()
358+
if err != nil || sourceSchema == nil {
359+
continue
360+
}
361+
362+
patchedSchema, err := convertToOpenAPISchema(sourceSchema)
363+
if err != nil {
364+
continue
365+
}
366+
367+
patchedSchema.VendorExtensible.Extensions = s.VendorExtensible.Extensions
368+
enhanceOpenAPIV3SchemaProperty(&patchedSchema)
369+
spec.Components.Schemas[name] = &patchedSchema
370+
}
371+
}
372+
373+
func convertToOpenAPISchema(source *apiextensionsv1.JSONSchemaProps) (validationspec.Schema, error) {
374+
bs, err := json.Marshal(source)
375+
if err != nil {
376+
return validationspec.Schema{}, err
377+
}
378+
379+
var result validationspec.Schema
380+
if err := json.Unmarshal(bs, &result); err != nil {
381+
return validationspec.Schema{}, err
382+
}
383+
384+
return result, nil
385+
}
386+
387+
func enhanceOpenAPIV3SchemaProperty(schema *validationspec.Schema) {
388+
specProp, ok := schema.Properties["spec"]
389+
if !ok {
390+
return
391+
}
392+
393+
versionsProp, ok := specProp.Properties["versions"]
394+
if !ok {
395+
return
396+
}
397+
398+
if versionsProp.Items == nil || versionsProp.Items.Schema == nil {
399+
return
400+
}
401+
402+
itemSchema := versionsProp.Items.Schema
403+
schemaProp, ok := itemSchema.Properties["schema"]
404+
if !ok {
405+
return
406+
}
407+
408+
oaProp, ok := schemaProp.Properties["openAPIV3Schema"]
409+
if !ok {
410+
return
411+
}
412+
413+
if len(oaProp.Properties) > 0 {
414+
return
415+
}
416+
if oaProp.Properties == nil {
417+
oaProp.Properties = make(map[string]validationspec.Schema)
418+
}
419+
420+
oaProp.Properties["type"] = validationspec.Schema{
421+
SchemaProps: validationspec.SchemaProps{Type: []string{"string"}},
422+
}
423+
oaProp.Properties["properties"] = validationspec.Schema{
424+
SchemaProps: validationspec.SchemaProps{
425+
Type: []string{"object"},
426+
AdditionalProperties: &validationspec.SchemaOrBool{Allows: true},
427+
},
428+
}
429+
430+
oaProp.AdditionalProperties = &validationspec.SchemaOrBool{Allows: true}
431+
schemaProp.Properties["openAPIV3Schema"] = oaProp
432+
itemSchema.Properties["schema"] = schemaProp
433+
versionsProp.Items.Schema = itemSchema
434+
specProp.Properties["versions"] = versionsProp
435+
schema.Properties["spec"] = specProp
436+
}
437+
322438
func groupVersionToOpenAPIV3Path(gv schema.GroupVersion) string {
323439
if gv.Group == "" {
324440
return "api/" + gv.Version

pkg/virtual/framework/dynamic/apiserver/serving_info.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ func CreateServingInfoFor(genericConfig genericapiserver.CompletedConfig, apiRes
7373
if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(openapiSchema, internalSchema, nil); err != nil {
7474
return nil, fmt.Errorf("failed converting CRD validation to internal version: %w", err)
7575
}
76+
77+
if isCRDAPIResourceSchema(apiResourceSchema) {
78+
patchCRDValidationSchema(internalSchema)
79+
}
80+
7681
structuralSchema, err := structuralschema.NewStructural(internalSchema)
7782
if err != nil {
7883
// This should never happen. If it does, it is a programming error.
@@ -404,3 +409,36 @@ func (c unstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object,
404409
ret.SetGroupVersionKind(kind)
405410
return ret, nil
406411
}
412+
413+
func patchCRDValidationSchema(schema *apiextensionsinternal.JSONSchemaProps) {
414+
trueVal := true
415+
416+
specProp, ok := schema.Properties["spec"]
417+
if !ok {
418+
return
419+
}
420+
421+
versionsProp, ok := specProp.Properties["versions"]
422+
if !ok {
423+
return
424+
}
425+
426+
if versionsProp.Items == nil || versionsProp.Items.Schema == nil {
427+
return
428+
}
429+
430+
itemSchema := versionsProp.Items.Schema
431+
schemaProp, ok := itemSchema.Properties["schema"]
432+
if !ok {
433+
return
434+
}
435+
436+
oaProp, ok := schemaProp.Properties["openAPIV3Schema"]
437+
if !ok {
438+
return
439+
}
440+
441+
oaProp.XPreserveUnknownFields = &trueVal
442+
schemaProp.Properties["openAPIV3Schema"] = oaProp
443+
itemSchema.Properties["schema"] = schemaProp
444+
}

staging/src/github.com/kcp-dev/sdk/testing/workspaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func NewLowLevelWorkspaceFixture[O WorkspaceOption](t TestingT, createClusterCli
136136
var err error
137137
ws, err = createClusterClient.Cluster(parent).TenancyV1alpha1().Workspaces().Create(ctx, tmpl, metav1.CreateOptions{})
138138
return err == nil, fmt.Sprintf("error creating workspace under %s: %v", parent, err)
139-
}, wait.ForeverTestTimeout, time.Millisecond*100, "failed to create %s workspace under %s", tmpl.Spec.Type.Name, parent)
139+
}, wait.ForeverTestTimeout*2, time.Millisecond*500, "failed to create %s workspace under %s", tmpl.Spec.Type.Name, parent)
140140

141141
wsName := ws.Name
142142
t.Cleanup(func() {

0 commit comments

Comments
 (0)