Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions pkg/util/proto/document_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ func (d *Definitions) ParseV3SchemaOrReference(s *openapi_v3.SchemaOrReference,
// ParseSchema creates a walkable Schema from an openapi v3 schema. While
// this function is public, it doesn't leak through the interface.
func (d *Definitions) ParseSchemaV3(s *openapi_v3.Schema, path *Path) (Schema, error) {
// Handle allOf-wrapped $ref: when a schema has no type but a single-element
// allOf containing a $ref, treat it as a reference. This is the OpenAPI 3.0
// compliant way to add sibling properties (description, default) alongside
// a $ref, since per the spec $ref siblings are ignored.
// We build the Ref from the outer schema so that description, default, and
// extensions are preserved.
if s.GetType() == "" && len(s.GetAllOf()) == 1 {
if ref, ok := s.GetAllOf()[0].GetOneof().(*openapi_v3.SchemaOrReference_Reference); ok {
xRef := ref.Reference.GetXRef()
if !strings.HasPrefix(xRef, "#/components/schemas") {
return d.parseV3Arbitrary(s, path)
}
reference := strings.TrimPrefix(xRef, "#/components/schemas/")
if _, ok := d.models[reference]; !ok {
return nil, newSchemaError(path, "unknown model in reference: %q", reference)
}
base, err := d.parseV3BaseSchema(s, path)
if err != nil {
return nil, err
}
return &Ref{
BaseSchema: *base,
reference: reference,
definitions: d,
}, nil
}
}

switch s.GetType() {
case object:
for _, extension := range s.GetSpecificationExtension() {
Expand Down
114 changes: 114 additions & 0 deletions pkg/util/proto/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,3 +752,117 @@ func TestV3GVKExtension(t *testing.T) {
}
})
}

func TestV3AllOfRef(t *testing.T) {
// In OpenAPI 3.0, sibling properties alongside $ref must be ignored.
// The spec-compliant way to combine $ref with description/default is
// to wrap the $ref in a single-element allOf. Kubernetes >= 1.35 uses
// this format (via kube-openapi's own builder3/util.WrapRefs).
spec := []byte(`{
"openapi": "3.0.0",
"info": {
"title": "Test",
"version": "v1"
},
"paths": {},
"components": {
"schemas": {
"Parent": {
"type": "object",
"properties": {
"directRef": {
"$ref": "#/components/schemas/Child"
},
"allOfRef": {
"allOf": [
{
"$ref": "#/components/schemas/Child"
}
],
"description": "field using allOf-wrapped ref",
"default": "some-default"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "test",
"kind": "Parent",
"version": "v1"
}
]
},
"Child": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "test",
"kind": "Child",
"version": "v1"
}
]
}
}
}
}`)

document, err := openapi_v3.ParseDocument(spec)
if err != nil {
t.Fatalf("failed to parse OpenAPI v3 document: %v", err)
}
models, err := proto.NewOpenAPIV3Data(document)
if err != nil {
t.Fatalf("failed to create OpenAPI v3 data: %v", err)
}

schema := models.LookupModel("Parent")
if schema == nil {
t.Fatal("model Parent not found")
}
parent, ok := schema.(*proto.Kind)
if !ok || parent == nil {
t.Fatal("expected Parent to be *proto.Kind")
}

t.Run("direct ref resolves as Reference", func(t *testing.T) {
field, ok := parent.Fields["directRef"]
if !ok {
t.Fatal("missing 'directRef' field")
}
ref, ok := field.(proto.Reference)
if !ok {
t.Fatalf("expected 'directRef' to be proto.Reference, got %T", field)
}
if ref.Reference() != "Child" {
t.Errorf("directRef reference = %q, want %q", ref.Reference(), "Child")
}
})

t.Run("allOf-wrapped ref resolves as Reference", func(t *testing.T) {
field, ok := parent.Fields["allOfRef"]
if !ok {
t.Fatal("missing 'allOfRef' field")
}
ref, ok := field.(proto.Reference)
if !ok {
t.Fatalf("expected 'allOfRef' to be proto.Reference, got %T", field)
}
if ref.Reference() != "Child" {
t.Errorf("allOfRef reference = %q, want %q", ref.Reference(), "Child")
}
})

t.Run("allOf-wrapped ref preserves description and default", func(t *testing.T) {
field := parent.Fields["allOfRef"]
if got := field.GetDescription(); got != "field using allOf-wrapped ref" {
t.Errorf("description = %q, want %q", got, "field using allOf-wrapped ref")
}
if got, want := field.GetDefault(), "some-default"; got != want {
t.Errorf("default = %v, want %v", got, want)
}
})
}