diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/definitions.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/definitions.go index 170b2c86b39..df7357f7785 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/definitions.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/definitions.go @@ -17,7 +17,6 @@ limitations under the License. package resolver import ( - "encoding/json" "fmt" "k8s.io/apimachinery/pkg/runtime" @@ -47,8 +46,9 @@ func NewDefinitionsSchemaResolver(scheme *runtime.Scheme, getDefinitions common. for name, def := range defs { _, e := namer.GetDefinitionName(name) gvks := extensionsToGVKs(e) + s := def.Schema // map value not addressable, make copy for _, gvk := range gvks { - gvkToSchema[gvk] = &def.Schema + gvkToSchema[gvk] = &s } } return &DefinitionsSchemaResolver{ @@ -62,45 +62,19 @@ func (d *DefinitionsSchemaResolver) ResolveSchema(gvk schema.GroupVersionKind) ( if !ok { return nil, fmt.Errorf("cannot resolve %v: %w", gvk, ErrSchemaNotFound) } - result, err := deepCopy(s) - if err != nil { - return nil, fmt.Errorf("cannot deep copy schema for %v: %v", gvk, err) - } - err = populateRefs(func(ref string) (*spec.Schema, bool) { + s, err := populateRefs(func(ref string) (*spec.Schema, bool) { // find the schema by the ref string, and return a deep copy def, ok := d.defs[ref] if !ok { return nil, false } - s, err := deepCopy(&def.Schema) - if err != nil { - return nil, false - } - return s, true - }, result) + s := def.Schema + return &s, true + }, s) if err != nil { return nil, err } - return result, nil -} - -// deepCopy generates a deep copy of the given schema with JSON marshalling and -// unmarshalling. -// The schema is expected to be "shallow", with all its field being Refs instead -// of nested schemas. -// If the schema contains cyclic reference, for example, a properties is itself -// it will return an error. This resolver does not support such condition. -func deepCopy(s *spec.Schema) (*spec.Schema, error) { - b, err := json.Marshal(s) - if err != nil { - return nil, err - } - result := new(spec.Schema) - err = json.Unmarshal(b, result) - if err != nil { - return nil, err - } - return result, nil + return s, nil } func extensionsToGVKs(extensions spec.Extensions) []schema.GroupVersionKind { diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/discovery.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/discovery.go index 75f9fd6dc99..53cbc7054b3 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/discovery.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/discovery.go @@ -57,7 +57,7 @@ func (r *ClientDiscoveryResolver) ResolveSchema(gvk schema.GroupVersionKind) (*s if err != nil { return nil, err } - err = populateRefs(func(ref string) (*spec.Schema, bool) { + s, err = populateRefs(func(ref string) (*spec.Schema, bool) { s, ok := resp.Components.Schemas[strings.TrimPrefix(ref, refPrefix)] return s, ok }, s) @@ -67,55 +67,6 @@ func (r *ClientDiscoveryResolver) ResolveSchema(gvk schema.GroupVersionKind) (*s return s, nil } -func populateRefs(schemaOf func(ref string) (*spec.Schema, bool), schema *spec.Schema) error { - ref, isRef := refOf(schema) - if isRef { - // replace the whole schema with the referred one. - resolved, ok := schemaOf(ref) - if !ok { - return fmt.Errorf("internal error: cannot resolve Ref %q: %w", ref, ErrSchemaNotFound) - } - *schema = *resolved - } - // schema is an object, populate its properties and additionalProperties - for name, prop := range schema.Properties { - err := populateRefs(schemaOf, &prop) - if err != nil { - return err - } - schema.Properties[name] = prop - } - if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil { - err := populateRefs(schemaOf, schema.AdditionalProperties.Schema) - if err != nil { - return err - } - } - // schema is a list, populate its items - if schema.Items != nil && schema.Items.Schema != nil { - err := populateRefs(schemaOf, schema.Items.Schema) - if err != nil { - return err - } - } - return nil -} - -func refOf(schema *spec.Schema) (string, bool) { - if schema.Ref.GetURL() != nil { - return schema.Ref.String(), true - } - // A Ref may be wrapped in allOf to preserve its description - // see https://github.com/kubernetes/kubernetes/issues/106387 - // For kube-openapi, allOf is only used for wrapping a Ref. - for _, allOf := range schema.AllOf { - if ref, isRef := refOf(&allOf); isRef { - return ref, isRef - } - } - return "", false -} - func resolveType(resp *schemaResponse, gvk schema.GroupVersionKind) (*spec.Schema, error) { for _, s := range resp.Components.Schemas { var gvks []schema.GroupVersionKind diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/refs.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/refs.go new file mode 100644 index 00000000000..49321bab47d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/refs.go @@ -0,0 +1,100 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resolver + +import ( + "fmt" + + "k8s.io/kube-openapi/pkg/validation/spec" +) + +// populateRefs recursively replaces Refs in the schema with the referred one. +// schemaOf is the callback to find the corresponding schema by the ref. +// This function will not mutate the original schema. If the schema needs to be +// mutated, a copy will be returned, otherwise it returns the original schema. +func populateRefs(schemaOf func(ref string) (*spec.Schema, bool), schema *spec.Schema) (*spec.Schema, error) { + result := *schema + changed := false + + ref, isRef := refOf(schema) + if isRef { + // replace the whole schema with the referred one. + resolved, ok := schemaOf(ref) + if !ok { + return nil, fmt.Errorf("internal error: cannot resolve Ref %q: %w", ref, ErrSchemaNotFound) + } + result = *resolved + changed = true + } + // schema is an object, populate its properties and additionalProperties + props := make(map[string]spec.Schema, len(schema.Properties)) + propsChanged := false + for name, prop := range result.Properties { + populated, err := populateRefs(schemaOf, &prop) + if err != nil { + return nil, err + } + if populated != &prop { + propsChanged = true + } + props[name] = *populated + } + if propsChanged { + changed = true + result.Properties = props + } + if result.AdditionalProperties != nil && result.AdditionalProperties.Schema != nil { + populated, err := populateRefs(schemaOf, result.AdditionalProperties.Schema) + if err != nil { + return nil, err + } + if populated != result.AdditionalProperties.Schema { + changed = true + result.AdditionalProperties.Schema = populated + } + } + // schema is a list, populate its items + if result.Items != nil && result.Items.Schema != nil { + populated, err := populateRefs(schemaOf, result.Items.Schema) + if err != nil { + return nil, err + } + if populated != result.Items.Schema { + changed = true + result.Items.Schema = populated + } + } + if changed { + return &result, nil + } + return schema, nil +} + +func refOf(schema *spec.Schema) (string, bool) { + if schema.Ref.GetURL() != nil { + return schema.Ref.String(), true + } + // A Ref may be wrapped in allOf to preserve its description + // see https://github.com/kubernetes/kubernetes/issues/106387 + // For kube-openapi, allOf is only used for wrapping a Ref. + for _, allOf := range schema.AllOf { + if ref, isRef := refOf(&allOf); isRef { + return ref, isRef + } + } + return "", false +}