mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-06 07:57:35 +00:00
Merge pull request #54181 from apelisse/update-kube-openapi
Automatic merge from submit-queue (batch tested with PRs 54199, 54181, 54196). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Update openapi to use kube-openapi code **What this PR does / why we need it**: OpenAPI code has moved to `github.com/kubernetes/kube-openapi`. Let's use that code as a dependency, since now it's duplicated. **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #51823 **Special notes for your reviewer**: **Release note**: ```release-note NONE ``` No user visible changes. Just code moving around.
This commit is contained in:
@@ -10,7 +10,6 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"document.go",
|
||||
"extensions.go",
|
||||
"openapi.go",
|
||||
"openapi_getter.go",
|
||||
@@ -19,9 +18,9 @@ go_library(
|
||||
deps = [
|
||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/client-go/discovery:go_default_library",
|
||||
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -43,6 +42,7 @@ go_test(
|
||||
"//vendor/github.com/onsi/ginkgo/types:go_default_library",
|
||||
"//vendor/github.com/onsi/gomega:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 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 openapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func newSchemaError(path *Path, format string, a ...interface{}) error {
|
||||
err := fmt.Sprintf(format, a...)
|
||||
if path.Len() == 0 {
|
||||
return fmt.Errorf("SchemaError: %v", err)
|
||||
}
|
||||
return fmt.Errorf("SchemaError(%v): %v", path, err)
|
||||
}
|
||||
|
||||
// groupVersionKindExtensionKey is the key used to lookup the
|
||||
// GroupVersionKind value for an object definition from the
|
||||
// definition's "extensions" map.
|
||||
const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
|
||||
|
||||
func vendorExtensionToMap(e []*openapi_v2.NamedAny) map[string]interface{} {
|
||||
values := map[string]interface{}{}
|
||||
|
||||
for _, na := range e {
|
||||
if na.GetName() == "" || na.GetValue() == nil {
|
||||
continue
|
||||
}
|
||||
if na.GetValue().GetYaml() == "" {
|
||||
continue
|
||||
}
|
||||
var value interface{}
|
||||
err := yaml.Unmarshal([]byte(na.GetValue().GetYaml()), &value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
values[na.GetName()] = value
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
|
||||
func parseGroupVersionKind(s *openapi_v2.Schema) schema.GroupVersionKind {
|
||||
extensionMap := vendorExtensionToMap(s.GetVendorExtension())
|
||||
|
||||
// Get the extensions
|
||||
gvkExtension, ok := extensionMap[groupVersionKindExtensionKey]
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
// gvk extension must be a list of 1 element.
|
||||
gvkList, ok := gvkExtension.([]interface{})
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
if len(gvkList) != 1 {
|
||||
return schema.GroupVersionKind{}
|
||||
|
||||
}
|
||||
gvk := gvkList[0]
|
||||
|
||||
// gvk extension list must be a map with group, version, and
|
||||
// kind fields
|
||||
gvkMap, ok := gvk.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
group, ok := gvkMap["group"].(string)
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
version, ok := gvkMap["version"].(string)
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
kind, ok := gvkMap["kind"].(string)
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
return schema.GroupVersionKind{
|
||||
Group: group,
|
||||
Version: version,
|
||||
Kind: kind,
|
||||
}
|
||||
}
|
||||
|
||||
// Definitions is an implementation of `Resources`. It looks for
|
||||
// resources in an openapi Schema.
|
||||
type Definitions struct {
|
||||
models map[string]Schema
|
||||
resources map[schema.GroupVersionKind]string
|
||||
}
|
||||
|
||||
var _ Resources = &Definitions{}
|
||||
|
||||
// NewOpenAPIData creates a new `Resources` out of the openapi document.
|
||||
func NewOpenAPIData(doc *openapi_v2.Document) (Resources, error) {
|
||||
definitions := Definitions{
|
||||
models: map[string]Schema{},
|
||||
resources: map[schema.GroupVersionKind]string{},
|
||||
}
|
||||
|
||||
// Save the list of all models first. This will allow us to
|
||||
// validate that we don't have any dangling reference.
|
||||
for _, namedSchema := range doc.GetDefinitions().GetAdditionalProperties() {
|
||||
definitions.models[namedSchema.GetName()] = nil
|
||||
}
|
||||
|
||||
// Now, parse each model. We can validate that references exists.
|
||||
for _, namedSchema := range doc.GetDefinitions().GetAdditionalProperties() {
|
||||
path := NewPath(namedSchema.GetName())
|
||||
schema, err := definitions.ParseSchema(namedSchema.GetValue(), &path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definitions.models[namedSchema.GetName()] = schema
|
||||
gvk := parseGroupVersionKind(namedSchema.GetValue())
|
||||
if len(gvk.Kind) > 0 {
|
||||
definitions.resources[gvk] = namedSchema.GetName()
|
||||
}
|
||||
}
|
||||
|
||||
return &definitions, nil
|
||||
}
|
||||
|
||||
// We believe the schema is a reference, verify that and returns a new
|
||||
// Schema
|
||||
func (d *Definitions) parseReference(s *openapi_v2.Schema, path *Path) (Schema, error) {
|
||||
if len(s.GetProperties().GetAdditionalProperties()) > 0 {
|
||||
return nil, newSchemaError(path, "unallowed embedded type definition")
|
||||
}
|
||||
if len(s.GetType().GetValue()) > 0 {
|
||||
return nil, newSchemaError(path, "definition reference can't have a type")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.GetXRef(), "#/definitions/") {
|
||||
return nil, newSchemaError(path, "unallowed reference to non-definition %q", s.GetXRef())
|
||||
}
|
||||
reference := strings.TrimPrefix(s.GetXRef(), "#/definitions/")
|
||||
if _, ok := d.models[reference]; !ok {
|
||||
return nil, newSchemaError(path, "unknown model in reference: %q", reference)
|
||||
}
|
||||
return &Ref{
|
||||
BaseSchema: d.parseBaseSchema(s, path),
|
||||
reference: reference,
|
||||
definitions: d,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Definitions) parseBaseSchema(s *openapi_v2.Schema, path *Path) BaseSchema {
|
||||
return BaseSchema{
|
||||
Description: s.GetDescription(),
|
||||
Extensions: vendorExtensionToMap(s.GetVendorExtension()),
|
||||
Path: *path,
|
||||
}
|
||||
}
|
||||
|
||||
// We believe the schema is a map, verify and return a new schema
|
||||
func (d *Definitions) parseMap(s *openapi_v2.Schema, path *Path) (Schema, error) {
|
||||
if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object {
|
||||
return nil, newSchemaError(path, "invalid object type")
|
||||
}
|
||||
if s.GetAdditionalProperties().GetSchema() == nil {
|
||||
return nil, newSchemaError(path, "invalid object doesn't have additional properties")
|
||||
}
|
||||
sub, err := d.ParseSchema(s.GetAdditionalProperties().GetSchema(), path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Map{
|
||||
BaseSchema: d.parseBaseSchema(s, path),
|
||||
SubType: sub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Definitions) parsePrimitive(s *openapi_v2.Schema, path *Path) (Schema, error) {
|
||||
var t string
|
||||
if len(s.GetType().GetValue()) > 1 {
|
||||
return nil, newSchemaError(path, "primitive can't have more than 1 type")
|
||||
}
|
||||
if len(s.GetType().GetValue()) == 1 {
|
||||
t = s.GetType().GetValue()[0]
|
||||
}
|
||||
switch t {
|
||||
case String:
|
||||
case Number:
|
||||
case Integer:
|
||||
case Boolean:
|
||||
case "": // Some models are completely empty, and can be safely ignored.
|
||||
// Do nothing
|
||||
default:
|
||||
return nil, newSchemaError(path, "Unknown primitive type: %q", t)
|
||||
}
|
||||
return &Primitive{
|
||||
BaseSchema: d.parseBaseSchema(s, path),
|
||||
Type: t,
|
||||
Format: s.GetFormat(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Definitions) parseArray(s *openapi_v2.Schema, path *Path) (Schema, error) {
|
||||
if len(s.GetType().GetValue()) != 1 {
|
||||
return nil, newSchemaError(path, "array should have exactly one type")
|
||||
}
|
||||
if s.GetType().GetValue()[0] != array {
|
||||
return nil, newSchemaError(path, `array should have type "array"`)
|
||||
}
|
||||
if len(s.GetItems().GetSchema()) != 1 {
|
||||
return nil, newSchemaError(path, "array should have exactly one sub-item")
|
||||
}
|
||||
sub, err := d.ParseSchema(s.GetItems().GetSchema()[0], path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Array{
|
||||
BaseSchema: d.parseBaseSchema(s, path),
|
||||
SubType: sub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Definitions) parseKind(s *openapi_v2.Schema, path *Path) (Schema, error) {
|
||||
if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object {
|
||||
return nil, newSchemaError(path, "invalid object type")
|
||||
}
|
||||
if s.GetProperties() == nil {
|
||||
return nil, newSchemaError(path, "object doesn't have properties")
|
||||
}
|
||||
|
||||
fields := map[string]Schema{}
|
||||
|
||||
for _, namedSchema := range s.GetProperties().GetAdditionalProperties() {
|
||||
var err error
|
||||
path := path.FieldPath(namedSchema.GetName())
|
||||
fields[namedSchema.GetName()], err = d.ParseSchema(namedSchema.GetValue(), &path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Kind{
|
||||
BaseSchema: d.parseBaseSchema(s, path),
|
||||
RequiredFields: s.GetRequired(),
|
||||
Fields: fields,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseSchema creates a walkable Schema from an openapi schema. While
|
||||
// this function is public, it doesn't leak through the interface.
|
||||
func (d *Definitions) ParseSchema(s *openapi_v2.Schema, path *Path) (Schema, error) {
|
||||
if len(s.GetType().GetValue()) == 1 {
|
||||
t := s.GetType().GetValue()[0]
|
||||
switch t {
|
||||
case object:
|
||||
return d.parseMap(s, path)
|
||||
case array:
|
||||
return d.parseArray(s, path)
|
||||
}
|
||||
|
||||
}
|
||||
if s.GetXRef() != "" {
|
||||
return d.parseReference(s, path)
|
||||
}
|
||||
if s.GetProperties() != nil {
|
||||
return d.parseKind(s, path)
|
||||
}
|
||||
return d.parsePrimitive(s, path)
|
||||
}
|
||||
|
||||
// LookupResource is public through the interface of Resources. It
|
||||
// returns a visitable schema from the given group-version-kind.
|
||||
func (d *Definitions) LookupResource(gvk schema.GroupVersionKind) Schema {
|
||||
modelName, found := d.resources[gvk]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
model, found := d.models[modelName]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
type Ref struct {
|
||||
BaseSchema
|
||||
|
||||
reference string
|
||||
definitions *Definitions
|
||||
}
|
||||
|
||||
var _ Reference = &Ref{}
|
||||
|
||||
func (r *Ref) Reference() string {
|
||||
return r.reference
|
||||
}
|
||||
|
||||
func (r *Ref) SubSchema() Schema {
|
||||
return r.definitions.models[r.reference]
|
||||
}
|
||||
|
||||
func (r *Ref) Accept(v SchemaVisitor) {
|
||||
v.VisitReference(r)
|
||||
}
|
||||
|
||||
func (r *Ref) GetName() string {
|
||||
return fmt.Sprintf("Reference to %q", r.reference)
|
||||
}
|
||||
@@ -17,236 +17,108 @@ limitations under the License.
|
||||
package openapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// Defines openapi types.
|
||||
const (
|
||||
Integer = "integer"
|
||||
Number = "number"
|
||||
String = "string"
|
||||
Boolean = "boolean"
|
||||
|
||||
// These types are private as they should never leak, and are
|
||||
// represented by actual structs.
|
||||
array = "array"
|
||||
object = "object"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
// Resources interface describe a resources provider, that can give you
|
||||
// resource based on group-version-kind.
|
||||
type Resources interface {
|
||||
LookupResource(gvk schema.GroupVersionKind) Schema
|
||||
LookupResource(gvk schema.GroupVersionKind) proto.Schema
|
||||
}
|
||||
|
||||
// SchemaVisitor is an interface that you need to implement if you want
|
||||
// to "visit" an openapi schema. A dispatch on the Schema type will call
|
||||
// the appropriate function based on its actual type:
|
||||
// - Array is a list of one and only one given subtype
|
||||
// - Map is a map of string to one and only one given subtype
|
||||
// - Primitive can be string, integer, number and boolean.
|
||||
// - Kind is an object with specific fields mapping to specific types.
|
||||
// - Reference is a link to another definition.
|
||||
type SchemaVisitor interface {
|
||||
VisitArray(*Array)
|
||||
VisitMap(*Map)
|
||||
VisitPrimitive(*Primitive)
|
||||
VisitKind(*Kind)
|
||||
VisitReference(Reference)
|
||||
// groupVersionKindExtensionKey is the key used to lookup the
|
||||
// GroupVersionKind value for an object definition from the
|
||||
// definition's "extensions" map.
|
||||
const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
|
||||
|
||||
// document is an implementation of `Resources`. It looks for
|
||||
// resources in an openapi Schema.
|
||||
type document struct {
|
||||
// Maps gvk to model name
|
||||
resources map[schema.GroupVersionKind]string
|
||||
models proto.Models
|
||||
}
|
||||
|
||||
// Schema is the base definition of an openapi type.
|
||||
type Schema interface {
|
||||
// Giving a visitor here will let you visit the actual type.
|
||||
Accept(SchemaVisitor)
|
||||
var _ Resources = &document{}
|
||||
|
||||
// Pretty print the name of the type.
|
||||
GetName() string
|
||||
// Describes how to access this field.
|
||||
GetPath() *Path
|
||||
// Describes the field.
|
||||
GetDescription() string
|
||||
// Returns type extensions.
|
||||
GetExtensions() map[string]interface{}
|
||||
}
|
||||
|
||||
// Path helps us keep track of type paths
|
||||
type Path struct {
|
||||
parent *Path
|
||||
key string
|
||||
}
|
||||
|
||||
func NewPath(key string) Path {
|
||||
return Path{key: key}
|
||||
}
|
||||
|
||||
func (p *Path) Get() []string {
|
||||
if p == nil {
|
||||
return []string{}
|
||||
func NewOpenAPIData(doc *openapi_v2.Document) (Resources, error) {
|
||||
models, err := proto.NewOpenAPIData(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.key == "" {
|
||||
return p.parent.Get()
|
||||
}
|
||||
return append(p.parent.Get(), p.key)
|
||||
}
|
||||
|
||||
func (p *Path) Len() int {
|
||||
return len(p.Get())
|
||||
}
|
||||
|
||||
func (p *Path) String() string {
|
||||
return strings.Join(p.Get(), "")
|
||||
}
|
||||
|
||||
// ArrayPath appends an array index and creates a new path
|
||||
func (p *Path) ArrayPath(i int) Path {
|
||||
return Path{
|
||||
parent: p,
|
||||
key: fmt.Sprintf("[%d]", i),
|
||||
}
|
||||
}
|
||||
|
||||
// FieldPath appends a field name and creates a new path
|
||||
func (p *Path) FieldPath(field string) Path {
|
||||
return Path{
|
||||
parent: p,
|
||||
key: fmt.Sprintf(".%s", field),
|
||||
}
|
||||
}
|
||||
|
||||
// BaseSchema holds data used by each types of schema.
|
||||
type BaseSchema struct {
|
||||
Description string
|
||||
Extensions map[string]interface{}
|
||||
|
||||
Path Path
|
||||
}
|
||||
|
||||
func (b *BaseSchema) GetDescription() string {
|
||||
return b.Description
|
||||
}
|
||||
|
||||
func (b *BaseSchema) GetExtensions() map[string]interface{} {
|
||||
return b.Extensions
|
||||
}
|
||||
|
||||
func (b *BaseSchema) GetPath() *Path {
|
||||
return &b.Path
|
||||
}
|
||||
|
||||
// Array must have all its element of the same `SubType`.
|
||||
type Array struct {
|
||||
BaseSchema
|
||||
|
||||
SubType Schema
|
||||
}
|
||||
|
||||
var _ Schema = &Array{}
|
||||
|
||||
func (a *Array) Accept(v SchemaVisitor) {
|
||||
v.VisitArray(a)
|
||||
}
|
||||
|
||||
func (a *Array) GetName() string {
|
||||
return fmt.Sprintf("Array of %s", a.SubType.GetName())
|
||||
}
|
||||
|
||||
// Kind is a complex object. It can have multiple different
|
||||
// subtypes for each field, as defined in the `Fields` field. Mandatory
|
||||
// fields are listed in `RequiredFields`. The key of the object is
|
||||
// always of type `string`.
|
||||
type Kind struct {
|
||||
BaseSchema
|
||||
|
||||
// Lists names of required fields.
|
||||
RequiredFields []string
|
||||
// Maps field names to types.
|
||||
Fields map[string]Schema
|
||||
}
|
||||
|
||||
var _ Schema = &Kind{}
|
||||
|
||||
func (k *Kind) Accept(v SchemaVisitor) {
|
||||
v.VisitKind(k)
|
||||
}
|
||||
|
||||
func (k *Kind) GetName() string {
|
||||
properties := []string{}
|
||||
for key := range k.Fields {
|
||||
properties = append(properties, key)
|
||||
}
|
||||
return fmt.Sprintf("Kind(%v)", properties)
|
||||
}
|
||||
|
||||
// IsRequired returns true if `field` is a required field for this type.
|
||||
func (k *Kind) IsRequired(field string) bool {
|
||||
for _, f := range k.RequiredFields {
|
||||
if f == field {
|
||||
return true
|
||||
resources := map[schema.GroupVersionKind]string{}
|
||||
for _, modelName := range models.ListModels() {
|
||||
model := models.LookupModel(modelName)
|
||||
if model == nil {
|
||||
panic("ListModels returns a model that can't be looked-up.")
|
||||
}
|
||||
gvk := parseGroupVersionKind(model)
|
||||
if len(gvk.Kind) > 0 {
|
||||
resources[gvk] = modelName
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
return &document{
|
||||
resources: resources,
|
||||
models: models,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Keys returns a alphabetically sorted list of keys.
|
||||
func (k *Kind) Keys() []string {
|
||||
keys := make([]string, 0)
|
||||
for key := range k.Fields {
|
||||
keys = append(keys, key)
|
||||
func (d *document) LookupResource(gvk schema.GroupVersionKind) proto.Schema {
|
||||
modelName, found := d.resources[gvk]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
return d.models.LookupModel(modelName)
|
||||
}
|
||||
|
||||
// Map is an object who values must all be of the same `SubType`.
|
||||
// The key of the object is always of type `string`.
|
||||
type Map struct {
|
||||
BaseSchema
|
||||
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
|
||||
func parseGroupVersionKind(s proto.Schema) schema.GroupVersionKind {
|
||||
extensions := s.GetExtensions()
|
||||
|
||||
SubType Schema
|
||||
}
|
||||
|
||||
var _ Schema = &Map{}
|
||||
|
||||
func (m *Map) Accept(v SchemaVisitor) {
|
||||
v.VisitMap(m)
|
||||
}
|
||||
|
||||
func (m *Map) GetName() string {
|
||||
return fmt.Sprintf("Map of %s", m.SubType.GetName())
|
||||
}
|
||||
|
||||
// Primitive is a literal. There can be multiple types of primitives,
|
||||
// and this subtype can be visited through the `subType` field.
|
||||
type Primitive struct {
|
||||
BaseSchema
|
||||
|
||||
// Type of a primitive must be one of: integer, number, string, boolean.
|
||||
Type string
|
||||
Format string
|
||||
}
|
||||
|
||||
var _ Schema = &Primitive{}
|
||||
|
||||
func (p *Primitive) Accept(v SchemaVisitor) {
|
||||
v.VisitPrimitive(p)
|
||||
}
|
||||
|
||||
func (p *Primitive) GetName() string {
|
||||
if p.Format == "" {
|
||||
return p.Type
|
||||
// Get the extensions
|
||||
gvkExtension, ok := extensions[groupVersionKindExtensionKey]
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", p.Type, p.Format)
|
||||
}
|
||||
|
||||
// Reference implementation depends on the type of document.
|
||||
type Reference interface {
|
||||
Schema
|
||||
// gvk extension must be a list of 1 element.
|
||||
gvkList, ok := gvkExtension.([]interface{})
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
if len(gvkList) != 1 {
|
||||
return schema.GroupVersionKind{}
|
||||
|
||||
Reference() string
|
||||
SubSchema() Schema
|
||||
}
|
||||
gvk := gvkList[0]
|
||||
|
||||
// gvk extension list must be a map with group, version, and
|
||||
// kind fields
|
||||
gvkMap, ok := gvk.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
group, ok := gvkMap["group"].(string)
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
version, ok := gvkMap["version"].(string)
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
kind, ok := gvkMap["kind"].(string)
|
||||
if !ok {
|
||||
return schema.GroupVersionKind{}
|
||||
}
|
||||
|
||||
return schema.GroupVersionKind{
|
||||
Group: group,
|
||||
Version: version,
|
||||
Kind: kind,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
tst "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/testing"
|
||||
)
|
||||
@@ -44,98 +45,17 @@ var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() {
|
||||
Group: "apps",
|
||||
}
|
||||
|
||||
var schema openapi.Schema
|
||||
var schema proto.Schema
|
||||
It("should lookup the Schema by its GroupVersionKind", func() {
|
||||
schema = resources.LookupResource(gvk)
|
||||
Expect(schema).ToNot(BeNil())
|
||||
})
|
||||
|
||||
var deployment *openapi.Kind
|
||||
var deployment *proto.Kind
|
||||
It("should be a Kind", func() {
|
||||
deployment = schema.(*openapi.Kind)
|
||||
deployment = schema.(*proto.Kind)
|
||||
Expect(deployment).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("should have a path", func() {
|
||||
Expect(deployment.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment"}))
|
||||
})
|
||||
|
||||
It("should have a kind key of type string", func() {
|
||||
Expect(deployment.Fields).To(HaveKey("kind"))
|
||||
key := deployment.Fields["kind"].(*openapi.Primitive)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Type).To(Equal("string"))
|
||||
Expect(key.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment", ".kind"}))
|
||||
})
|
||||
|
||||
It("should have a apiVersion key of type string", func() {
|
||||
Expect(deployment.Fields).To(HaveKey("apiVersion"))
|
||||
key := deployment.Fields["apiVersion"].(*openapi.Primitive)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Type).To(Equal("string"))
|
||||
Expect(key.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment", ".apiVersion"}))
|
||||
})
|
||||
|
||||
It("should have a metadata key of type Reference", func() {
|
||||
Expect(deployment.Fields).To(HaveKey("metadata"))
|
||||
key := deployment.Fields["metadata"].(openapi.Reference)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Reference()).To(Equal("io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"))
|
||||
subSchema := key.SubSchema().(*openapi.Kind)
|
||||
Expect(subSchema).ToNot(BeNil())
|
||||
})
|
||||
|
||||
var status *openapi.Kind
|
||||
It("should have a status key of type Reference", func() {
|
||||
Expect(deployment.Fields).To(HaveKey("status"))
|
||||
key := deployment.Fields["status"].(openapi.Reference)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Reference()).To(Equal("io.k8s.api.apps.v1beta1.DeploymentStatus"))
|
||||
status = key.SubSchema().(*openapi.Kind)
|
||||
Expect(status).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("should have a valid DeploymentStatus", func() {
|
||||
By("having availableReplicas key")
|
||||
Expect(status.Fields).To(HaveKey("availableReplicas"))
|
||||
replicas := status.Fields["availableReplicas"].(*openapi.Primitive)
|
||||
Expect(replicas).ToNot(BeNil())
|
||||
Expect(replicas.Type).To(Equal("integer"))
|
||||
|
||||
By("having conditions key")
|
||||
Expect(status.Fields).To(HaveKey("conditions"))
|
||||
conditions := status.Fields["conditions"].(*openapi.Array)
|
||||
Expect(conditions).ToNot(BeNil())
|
||||
Expect(conditions.GetName()).To(Equal(`Array of Reference to "io.k8s.api.apps.v1beta1.DeploymentCondition"`))
|
||||
Expect(conditions.GetExtensions()).To(Equal(map[string]interface{}{
|
||||
"x-kubernetes-patch-merge-key": "type",
|
||||
"x-kubernetes-patch-strategy": "merge",
|
||||
}))
|
||||
condition := conditions.SubType.(openapi.Reference)
|
||||
Expect(condition.Reference()).To(Equal("io.k8s.api.apps.v1beta1.DeploymentCondition"))
|
||||
})
|
||||
|
||||
var spec *openapi.Kind
|
||||
It("should have a spec key of type Reference", func() {
|
||||
Expect(deployment.Fields).To(HaveKey("spec"))
|
||||
key := deployment.Fields["spec"].(openapi.Reference)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Reference()).To(Equal("io.k8s.api.apps.v1beta1.DeploymentSpec"))
|
||||
spec = key.SubSchema().(*openapi.Kind)
|
||||
Expect(spec).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("should have a spec with no gvk", func() {
|
||||
_, found := spec.GetExtensions()["x-kubernetes-group-version-kind"]
|
||||
Expect(found).To(BeFalse())
|
||||
})
|
||||
|
||||
It("should have a spec with a PodTemplateSpec sub-field", func() {
|
||||
Expect(spec.Fields).To(HaveKey("template"))
|
||||
key := spec.Fields["template"].(openapi.Reference)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Reference()).To(Equal("io.k8s.api.core.v1.PodTemplateSpec"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Reading authorization.k8s.io/v1/SubjectAccessReview from openAPIData", func() {
|
||||
@@ -153,66 +73,21 @@ var _ = Describe("Reading authorization.k8s.io/v1/SubjectAccessReview from openA
|
||||
Group: "authorization.k8s.io",
|
||||
}
|
||||
|
||||
var schema openapi.Schema
|
||||
var schema proto.Schema
|
||||
It("should lookup the Schema by its GroupVersionKind", func() {
|
||||
schema = resources.LookupResource(gvk)
|
||||
Expect(schema).ToNot(BeNil())
|
||||
})
|
||||
|
||||
var sarspec *openapi.Kind
|
||||
var sarspec *proto.Kind
|
||||
It("should be a Kind and have a spec", func() {
|
||||
sar := schema.(*openapi.Kind)
|
||||
sar := schema.(*proto.Kind)
|
||||
Expect(sar).ToNot(BeNil())
|
||||
Expect(sar.Fields).To(HaveKey("spec"))
|
||||
specRef := sar.Fields["spec"].(openapi.Reference)
|
||||
specRef := sar.Fields["spec"].(proto.Reference)
|
||||
Expect(specRef).ToNot(BeNil())
|
||||
Expect(specRef.Reference()).To(Equal("io.k8s.api.authorization.v1.SubjectAccessReviewSpec"))
|
||||
sarspec = specRef.SubSchema().(*openapi.Kind)
|
||||
sarspec = specRef.SubSchema().(*proto.Kind)
|
||||
Expect(sarspec).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("should have a valid SubjectAccessReviewSpec", func() {
|
||||
Expect(sarspec.Fields).To(HaveKey("extra"))
|
||||
extra := sarspec.Fields["extra"].(*openapi.Map)
|
||||
Expect(extra).ToNot(BeNil())
|
||||
Expect(extra.GetName()).To(Equal("Map of Array of string"))
|
||||
Expect(extra.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", ".extra"}))
|
||||
array := extra.SubType.(*openapi.Array)
|
||||
Expect(array).ToNot(BeNil())
|
||||
Expect(array.GetName()).To(Equal("Array of string"))
|
||||
Expect(array.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", ".extra"}))
|
||||
str := array.SubType.(*openapi.Primitive)
|
||||
Expect(str).ToNot(BeNil())
|
||||
Expect(str.Type).To(Equal("string"))
|
||||
Expect(str.GetName()).To(Equal("string"))
|
||||
Expect(str.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", ".extra"}))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Path", func() {
|
||||
It("can be created by NewPath", func() {
|
||||
path := openapi.NewPath("key")
|
||||
Expect(path.String()).To(Equal("key"))
|
||||
})
|
||||
It("can create and print complex paths", func() {
|
||||
key := openapi.NewPath("key")
|
||||
array := key.ArrayPath(12)
|
||||
field := array.FieldPath("subKey")
|
||||
|
||||
Expect(field.String()).To(Equal("key[12].subKey"))
|
||||
})
|
||||
It("has a length", func() {
|
||||
key := openapi.NewPath("key")
|
||||
array := key.ArrayPath(12)
|
||||
field := array.FieldPath("subKey")
|
||||
|
||||
Expect(field.Len()).To(Equal(3))
|
||||
})
|
||||
It("can look like an array", func() {
|
||||
key := openapi.NewPath("key")
|
||||
array := key.ArrayPath(12)
|
||||
field := array.FieldPath("subKey")
|
||||
|
||||
Expect(field.Get()).To(Equal([]string{"key", "[12]", ".subKey"}))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ go_library(
|
||||
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
|
||||
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
@@ -109,7 +110,7 @@ func NewFakeResources(path string) *FakeResources {
|
||||
|
||||
// LookupResource will read the schema, parse it and return the
|
||||
// resources. It doesn't return errors and will panic instead.
|
||||
func (f *FakeResources) LookupResource(gvk schema.GroupVersionKind) openapi.Schema {
|
||||
func (f *FakeResources) LookupResource(gvk schema.GroupVersionKind) proto.Schema {
|
||||
s, err := f.fake.OpenAPISchema()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -20,6 +20,7 @@ go_library(
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
|
||||
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -20,19 +20,19 @@ import (
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
type ValidationItem interface {
|
||||
openapi.SchemaVisitor
|
||||
proto.SchemaVisitor
|
||||
|
||||
Errors() []error
|
||||
Path() *openapi.Path
|
||||
Path() *proto.Path
|
||||
}
|
||||
|
||||
type baseItem struct {
|
||||
errors Errors
|
||||
path openapi.Path
|
||||
path proto.Path
|
||||
}
|
||||
|
||||
// Errors returns the list of errors found for this item.
|
||||
@@ -58,7 +58,7 @@ func (item *baseItem) CopyErrors(errs []error) {
|
||||
}
|
||||
|
||||
// Path returns the path of this item, helps print useful errors.
|
||||
func (item *baseItem) Path() *openapi.Path {
|
||||
func (item *baseItem) Path() *proto.Path {
|
||||
return &item.path
|
||||
}
|
||||
|
||||
@@ -80,15 +80,15 @@ func (item *mapItem) sortedKeys() []string {
|
||||
|
||||
var _ ValidationItem = &mapItem{}
|
||||
|
||||
func (item *mapItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
func (item *mapItem) VisitPrimitive(schema *proto.Primitive) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "map"})
|
||||
}
|
||||
|
||||
func (item *mapItem) VisitArray(schema *openapi.Array) {
|
||||
func (item *mapItem) VisitArray(schema *proto.Array) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "map"})
|
||||
}
|
||||
|
||||
func (item *mapItem) VisitMap(schema *openapi.Map) {
|
||||
func (item *mapItem) VisitMap(schema *proto.Map) {
|
||||
for _, key := range item.sortedKeys() {
|
||||
subItem, err := itemFactory(item.Path().FieldPath(key), item.Map[key])
|
||||
if err != nil {
|
||||
@@ -100,7 +100,7 @@ func (item *mapItem) VisitMap(schema *openapi.Map) {
|
||||
}
|
||||
}
|
||||
|
||||
func (item *mapItem) VisitKind(schema *openapi.Kind) {
|
||||
func (item *mapItem) VisitKind(schema *proto.Kind) {
|
||||
// Verify each sub-field.
|
||||
for _, key := range item.sortedKeys() {
|
||||
if item.Map[key] == nil {
|
||||
@@ -127,7 +127,7 @@ func (item *mapItem) VisitKind(schema *openapi.Kind) {
|
||||
}
|
||||
}
|
||||
|
||||
func (item *mapItem) VisitReference(schema openapi.Reference) {
|
||||
func (item *mapItem) VisitReference(schema proto.Reference) {
|
||||
// passthrough
|
||||
schema.SubSchema().Accept(item)
|
||||
}
|
||||
@@ -141,11 +141,11 @@ type arrayItem struct {
|
||||
|
||||
var _ ValidationItem = &arrayItem{}
|
||||
|
||||
func (item *arrayItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
func (item *arrayItem) VisitPrimitive(schema *proto.Primitive) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "array"})
|
||||
}
|
||||
|
||||
func (item *arrayItem) VisitArray(schema *openapi.Array) {
|
||||
func (item *arrayItem) VisitArray(schema *proto.Array) {
|
||||
for i, v := range item.Array {
|
||||
path := item.Path().ArrayPath(i)
|
||||
if v == nil {
|
||||
@@ -162,15 +162,15 @@ func (item *arrayItem) VisitArray(schema *openapi.Array) {
|
||||
}
|
||||
}
|
||||
|
||||
func (item *arrayItem) VisitMap(schema *openapi.Map) {
|
||||
func (item *arrayItem) VisitMap(schema *proto.Map) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "map"})
|
||||
}
|
||||
|
||||
func (item *arrayItem) VisitKind(schema *openapi.Kind) {
|
||||
func (item *arrayItem) VisitKind(schema *proto.Kind) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "map"})
|
||||
}
|
||||
|
||||
func (item *arrayItem) VisitReference(schema openapi.Reference) {
|
||||
func (item *arrayItem) VisitReference(schema proto.Reference) {
|
||||
// passthrough
|
||||
schema.SubSchema().Accept(item)
|
||||
}
|
||||
@@ -185,54 +185,54 @@ type primitiveItem struct {
|
||||
|
||||
var _ ValidationItem = &primitiveItem{}
|
||||
|
||||
func (item *primitiveItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
func (item *primitiveItem) VisitPrimitive(schema *proto.Primitive) {
|
||||
// Some types of primitives can match more than one (a number
|
||||
// can be a string, but not the other way around). Return from
|
||||
// the switch if we have a valid possible type conversion
|
||||
// NOTE(apelisse): This logic is blindly copied from the
|
||||
// existing swagger logic, and I'm not sure I agree with it.
|
||||
switch schema.Type {
|
||||
case openapi.Boolean:
|
||||
case proto.Boolean:
|
||||
switch item.Kind {
|
||||
case openapi.Boolean:
|
||||
case proto.Boolean:
|
||||
return
|
||||
}
|
||||
case openapi.Integer:
|
||||
case proto.Integer:
|
||||
switch item.Kind {
|
||||
case openapi.Integer, openapi.Number:
|
||||
case proto.Integer, proto.Number:
|
||||
return
|
||||
}
|
||||
case openapi.Number:
|
||||
case proto.Number:
|
||||
switch item.Kind {
|
||||
case openapi.Number:
|
||||
case proto.Number:
|
||||
return
|
||||
}
|
||||
case openapi.String:
|
||||
case proto.String:
|
||||
return
|
||||
}
|
||||
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: item.Kind})
|
||||
}
|
||||
|
||||
func (item *primitiveItem) VisitArray(schema *openapi.Array) {
|
||||
func (item *primitiveItem) VisitArray(schema *proto.Array) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: item.Kind})
|
||||
}
|
||||
|
||||
func (item *primitiveItem) VisitMap(schema *openapi.Map) {
|
||||
func (item *primitiveItem) VisitMap(schema *proto.Map) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: item.Kind})
|
||||
}
|
||||
|
||||
func (item *primitiveItem) VisitKind(schema *openapi.Kind) {
|
||||
func (item *primitiveItem) VisitKind(schema *proto.Kind) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: item.Kind})
|
||||
}
|
||||
|
||||
func (item *primitiveItem) VisitReference(schema openapi.Reference) {
|
||||
func (item *primitiveItem) VisitReference(schema proto.Reference) {
|
||||
// passthrough
|
||||
schema.SubSchema().Accept(item)
|
||||
}
|
||||
|
||||
// itemFactory creates the relevant item type/visitor based on the current yaml type.
|
||||
func itemFactory(path openapi.Path, v interface{}) (ValidationItem, error) {
|
||||
func itemFactory(path proto.Path, v interface{}) (ValidationItem, error) {
|
||||
// We need to special case for no-type fields in yaml (e.g. empty item in list)
|
||||
if v == nil {
|
||||
return nil, InvalidObjectTypeError{Type: "nil", Path: path.String()}
|
||||
@@ -243,7 +243,7 @@ func itemFactory(path openapi.Path, v interface{}) (ValidationItem, error) {
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.Boolean,
|
||||
Kind: proto.Boolean,
|
||||
}, nil
|
||||
case reflect.Int,
|
||||
reflect.Int8,
|
||||
@@ -258,20 +258,20 @@ func itemFactory(path openapi.Path, v interface{}) (ValidationItem, error) {
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.Integer,
|
||||
Kind: proto.Integer,
|
||||
}, nil
|
||||
case reflect.Float32,
|
||||
reflect.Float64:
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.Number,
|
||||
Kind: proto.Number,
|
||||
}, nil
|
||||
case reflect.String:
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.String,
|
||||
Kind: proto.String,
|
||||
}, nil
|
||||
case reflect.Array,
|
||||
reflect.Slice:
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
@@ -79,7 +80,7 @@ func (v *SchemaValidation) validateResource(obj interface{}, gvk schema.GroupVer
|
||||
return nil
|
||||
}
|
||||
|
||||
rootValidation, err := itemFactory(openapi.NewPath(gvk.Kind), obj)
|
||||
rootValidation, err := itemFactory(proto.NewPath(gvk.Kind), obj)
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user