mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
Preserve externalDocs and example in CRD structural schema conversion
Thread externalDocs and example fields through the structural schema so they flow to published OpenAPI specs. Previously, both were intentionally dropped during JSONSchemaProps to Structural conversion, making them unavailable to clients like kubectl explain.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user