diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert.go index 78350c135fc..a4a35354088 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert.go @@ -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 } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert_test.go index 5eb7fba2727..e16f0f2cad5 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert_test.go @@ -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 diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi.go index df78ba77e61..a8b29a1e61d 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi.go @@ -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) { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi_test.go index b327e52d561..1ffc89e1f66 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/kubeopenapi_test.go @@ -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) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/structural.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/structural.go index 5688c2ac40b..9965718b014 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/structural.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/structural.go @@ -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 diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation_test.go index a83e50ff958..734cec1325f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation_test.go @@ -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()) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/zz_generated.deepcopy.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/zz_generated.deepcopy.go index 9cec0262b53..c2154a05fd5 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/zz_generated.deepcopy.go @@ -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 } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/v2/conversion_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/v2/conversion_test.go index 76363d945b4..3f375956217 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/v2/conversion_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/v2/conversion_test.go @@ -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",