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 new file mode 100644 index 00000000000..170b2c86b39 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/definitions.go @@ -0,0 +1,141 @@ +/* +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 ( + "encoding/json" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/openapi" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +// DefinitionsSchemaResolver resolves the schema of a built-in type +// by looking up the OpenAPI definitions. +type DefinitionsSchemaResolver struct { + defs map[string]common.OpenAPIDefinition + gvkToSchema map[schema.GroupVersionKind]*spec.Schema +} + +// NewDefinitionsSchemaResolver creates a new DefinitionsSchemaResolver. +// An example working setup: +// scheme = "k8s.io/client-go/kubernetes/scheme".Scheme +// getDefinitions = "k8s.io/kubernetes/pkg/generated/openapi".GetOpenAPIDefinitions +func NewDefinitionsSchemaResolver(scheme *runtime.Scheme, getDefinitions common.GetOpenAPIDefinitions) *DefinitionsSchemaResolver { + gvkToSchema := make(map[schema.GroupVersionKind]*spec.Schema) + namer := openapi.NewDefinitionNamer(scheme) + defs := getDefinitions(func(path string) spec.Ref { + return spec.MustCreateRef(path) + }) + for name, def := range defs { + _, e := namer.GetDefinitionName(name) + gvks := extensionsToGVKs(e) + for _, gvk := range gvks { + gvkToSchema[gvk] = &def.Schema + } + } + return &DefinitionsSchemaResolver{ + gvkToSchema: gvkToSchema, + defs: defs, + } +} + +func (d *DefinitionsSchemaResolver) ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) { + s, ok := d.gvkToSchema[gvk] + 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) { + // 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) + 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 +} + +func extensionsToGVKs(extensions spec.Extensions) []schema.GroupVersionKind { + gvksAny, ok := extensions[extGVK] + if !ok { + return nil + } + gvks, ok := gvksAny.([]any) + if !ok { + return nil + } + result := make([]schema.GroupVersionKind, 0, len(gvks)) + for _, gvkAny := range gvks { + // type check the map and all fields + gvkMap, ok := gvkAny.(map[string]any) + if !ok { + return nil + } + g, ok := gvkMap["group"].(string) + if !ok { + return nil + } + v, ok := gvkMap["version"].(string) + if !ok { + return nil + } + k, ok := gvkMap["kind"].(string) + if !ok { + return nil + } + result = append(result, schema.GroupVersionKind{ + Group: g, + Version: v, + Kind: k, + }) + } + return result +} 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 new file mode 100644 index 00000000000..75f9fd6dc99 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/discovery.go @@ -0,0 +1,153 @@ +/* +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 ( + "encoding/json" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +// ClientDiscoveryResolver uses client-go discovery to resolve schemas at run time. +type ClientDiscoveryResolver struct { + Discovery discovery.DiscoveryInterface +} + +var _ SchemaResolver = (*ClientDiscoveryResolver)(nil) + +func (r *ClientDiscoveryResolver) ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) { + p, err := r.Discovery.OpenAPIV3().Paths() + if err != nil { + return nil, err + } + resourcePath := resourcePathFromGV(gvk.GroupVersion()) + c, ok := p[resourcePath] + if !ok { + return nil, fmt.Errorf("cannot resolve group version %q: %w", gvk.GroupVersion(), ErrSchemaNotFound) + } + b, err := c.Schema(runtime.ContentTypeJSON) + if err != nil { + return nil, err + } + resp := new(schemaResponse) + err = json.Unmarshal(b, resp) + if err != nil { + return nil, err + } + s, err := resolveType(resp, gvk) + if err != nil { + return nil, err + } + err = populateRefs(func(ref string) (*spec.Schema, bool) { + s, ok := resp.Components.Schemas[strings.TrimPrefix(ref, refPrefix)] + return s, ok + }, s) + if err != nil { + return nil, err + } + 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 + err := s.Extensions.GetObject(extGVK, &gvks) + if err != nil { + return nil, err + } + for _, g := range gvks { + if g == gvk { + return s, nil + } + } + } + return nil, fmt.Errorf("cannot resolve group version kind %q: %w", gvk, ErrSchemaNotFound) +} + +func resourcePathFromGV(gv schema.GroupVersion) string { + var resourcePath string + if len(gv.Group) == 0 { + resourcePath = fmt.Sprintf("api/%s", gv.Version) + } else { + resourcePath = fmt.Sprintf("apis/%s/%s", gv.Group, gv.Version) + } + return resourcePath +} + +type schemaResponse struct { + Components struct { + Schemas map[string]*spec.Schema `json:"schemas"` + } `json:"components"` +} + +const refPrefix = "#/components/schemas/" + +const extGVK = "x-kubernetes-group-version-kind" diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/resolver.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/resolver.go new file mode 100644 index 00000000000..4060c86cefc --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/resolver.go @@ -0,0 +1,39 @@ +/* +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/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +// SchemaResolver finds the OpenAPI schema for the given GroupVersionKind. +// This interface uses the type defined by k8s.io/kube-openapi +type SchemaResolver interface { + // ResolveSchema takes a GroupVersionKind (GVK) and returns the OpenAPI schema + // identified by the GVK. + // The function returns a non-nil error if the schema cannot be found or fail + // to resolve. The returned error wraps ErrSchemaNotFound if the resolution is + // attempted but the corresponding schema cannot be found. + ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) +} + +// ErrSchemaNotFound is wrapped and returned if the schema cannot be located +// by the resolver. +var ErrSchemaNotFound = fmt.Errorf("schema not found")