Merge pull request #136977 from pedjak/allow-extradocs-openapi-schema-response

Preserve externalDocs and example in CRD structural schema conversion
This commit is contained in:
Kubernetes Prow Robot
2026-03-18 02:33:40 +05:30
committed by GitHub
8 changed files with 82 additions and 16 deletions

View File

@@ -35,10 +35,6 @@ import (
// - dependencies
// - additionalItems
// - definitions.
//
// The follow fields are not preserved:
// - externalDocs
// - example.
func NewStructural(s *apiextensions.JSONSchemaProps) (*Structural, error) {
if s == nil {
return nil, nil
@@ -126,6 +122,15 @@ func newGenerics(s *apiextensions.JSONSchemaProps) (*Generic, error) {
if s.Default != nil {
g.Default = JSON{interface{}(*s.Default)}
}
if s.ExternalDocs != nil {
g.ExternalDocs = &ExternalDocumentation{
Description: s.ExternalDocs.Description,
URL: s.ExternalDocs.URL,
}
}
if s.Example != nil {
g.Example = JSON{interface{}(*s.Example)}
}
return g, nil
}

View File

@@ -70,10 +70,6 @@ func TestStructuralRoundtripOrError(t *testing.T) {
origSchema := &apiextensions.JSONSchemaProps{}
x := reflect.ValueOf(origSchema).Elem()
n := rand.Intn(x.NumField())
if name := x.Type().Field(n).Name; name == "Example" || name == "ExternalDocs" {
// we drop these intentionally
continue
}
f.Fill(x.Field(n).Addr().Interface())
// it roundtrips or NewStructural errors out. We should never drop anything

View File

@@ -62,6 +62,13 @@ func (g *Generic) toKubeOpenAPI(ret *spec.Schema) {
ret.Description = g.Description
ret.Title = g.Title
ret.Default = g.Default.Object
if g.ExternalDocs != nil {
ret.ExternalDocs = &spec.ExternalDocumentation{
Description: g.ExternalDocs.Description,
URL: g.ExternalDocs.URL,
}
}
ret.Example = g.Example.Object
}
func (x *Extensions) toKubeOpenAPI(ret *spec.Schema) {

View File

@@ -30,6 +30,23 @@ import (
"k8s.io/apimachinery/pkg/util/json"
)
// isJSONEmptyForOmitempty returns true if the JSON value would be dropped by the
// kube-openapi v2 JSON encoder's omitempty tag. Unlike omitzero (used for Default),
// the Example field uses omitempty which considers empty strings, empty slices,
// and empty maps as empty.
func isJSONEmptyForOmitempty(j JSON) bool {
switch v := j.Object.(type) {
case string:
return v == ""
case []interface{}:
return len(v) == 0
case map[string]interface{}:
return len(v) == 0
default:
return j.Object == nil
}
}
func TestStructuralKubeOpenAPIRoundtrip(t *testing.T) {
f := randfill.New()
seed := time.Now().UnixNano()
@@ -64,13 +81,21 @@ func TestStructuralKubeOpenAPIRoundtrip(t *testing.T) {
f.Fill(orig)
// normalize Structural.ValueValidation to zero values if it was nil before
// and normalize Example values that are considered empty by omitempty
// (the kube-openapi Example field uses omitempty, not omitzero, so
// empty strings/slices/maps get dropped during JSON roundtrip)
normalizer := Visitor{
Structural: func(s *Structural) bool {
changed := false
if s.ValueValidation == nil {
s.ValueValidation = &ValueValidation{}
return true
changed = true
}
return false
if isJSONEmptyForOmitempty(s.Generic.Example) {
s.Generic.Example = JSON{}
changed = true
}
return changed
},
}
normalizer.Visit(orig)

View File

@@ -46,6 +46,14 @@ type StructuralOrBool struct {
// +k8s:deepcopy-gen=true
// ExternalDocumentation allows referencing an external resource for extended documentation.
type ExternalDocumentation struct {
Description string
URL string
}
// +k8s:deepcopy-gen=true
// Generic contains the generic schema fields not allowed in value validation.
type Generic struct {
Description string
@@ -53,10 +61,12 @@ type Generic struct {
// It can be object, array, number, integer, boolean, string.
// It is optional only if x-kubernetes-preserve-unknown-fields
// or x-kubernetes-int-or-string is true.
Type string
Title string
Default JSON
Nullable bool
Type string
Title string
Default JSON
Nullable bool
ExternalDocs *ExternalDocumentation
Example JSON
}
// +k8s:deepcopy-gen=true

View File

@@ -402,6 +402,9 @@ func TestValidateNestedValueValidationComplete(t *testing.T) {
// check that we didn't forget to check any forbidden generic field
tt := reflect.TypeOf(Generic{})
for i := 0; i < tt.NumField(); i++ {
if name := tt.Field(i).Name; name == "ExternalDocs" || name == "Example" {
continue // allowed in nested value validations
}
vv := &NestedValueValidation{}
x := reflect.ValueOf(&vv.ForbiddenGenerics).Elem()
fuzzer.Fill(x.Field(i).Addr().Interface())

View File

@@ -56,10 +56,32 @@ func (in *Extensions) DeepCopy() *Extensions {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalDocumentation) DeepCopyInto(out *ExternalDocumentation) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDocumentation.
func (in *ExternalDocumentation) DeepCopy() *ExternalDocumentation {
if in == nil {
return nil
}
out := new(ExternalDocumentation)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Generic) DeepCopyInto(out *Generic) {
*out = *in
out.Default = in.Default.DeepCopy()
if in.ExternalDocs != nil {
in, out := &in.ExternalDocs, &out.ExternalDocs
*out = new(ExternalDocumentation)
**out = **in
}
out.Example = in.Example.DeepCopy()
return
}

View File

@@ -632,7 +632,6 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaByType(t *testing.T) {
},
expected: new(spec.Schema).
WithExternalDocs(testStr, testStr2),
expectDiff: true,
},
{
name: "example",
@@ -641,7 +640,6 @@ func Test_ConvertJSONSchemaPropsToOpenAPIv2SchemaByType(t *testing.T) {
},
expected: new(spec.Schema).
WithExample(testStr),
expectDiff: true,
},
{
name: "preserve-unknown-fields in arrays",