Merge pull request #120707 from Jefftree/csa-openapiv3

Use OpenAPI V3 for client side SMP
This commit is contained in:
Kubernetes Prow Robot 2023-10-31 20:23:27 +01:00 committed by GitHub
commit 07d2da75bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 16976 additions and 525 deletions

View File

@ -20,12 +20,17 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"k8s.io/apimachinery/pkg/util/mergepatch" "k8s.io/apimachinery/pkg/util/mergepatch"
forkedjson "k8s.io/apimachinery/third_party/forked/golang/json" forkedjson "k8s.io/apimachinery/third_party/forked/golang/json"
openapi "k8s.io/kube-openapi/pkg/util/proto" openapi "k8s.io/kube-openapi/pkg/util/proto"
"k8s.io/kube-openapi/pkg/validation/spec"
) )
const patchMergeKey = "x-kubernetes-patch-merge-key"
const patchStrategy = "x-kubernetes-patch-strategy"
type PatchMeta struct { type PatchMeta struct {
patchStrategies []string patchStrategies []string
patchMergeKey string patchMergeKey string
@ -148,6 +153,90 @@ func GetTagStructTypeOrDie(dataStruct interface{}) reflect.Type {
return t return t
} }
type PatchMetaFromOpenAPIV3 struct {
// SchemaList is required to resolve OpenAPI V3 references
SchemaList map[string]*spec.Schema
Schema *spec.Schema
}
func (s PatchMetaFromOpenAPIV3) traverse(key string) (PatchMetaFromOpenAPIV3, error) {
if s.Schema == nil {
return PatchMetaFromOpenAPIV3{}, nil
}
if len(s.Schema.Properties) == 0 {
return PatchMetaFromOpenAPIV3{}, fmt.Errorf("unable to find api field \"%s\"", key)
}
subschema, ok := s.Schema.Properties[key]
if !ok {
return PatchMetaFromOpenAPIV3{}, fmt.Errorf("unable to find api field \"%s\"", key)
}
return PatchMetaFromOpenAPIV3{SchemaList: s.SchemaList, Schema: &subschema}, nil
}
func resolve(l *PatchMetaFromOpenAPIV3) error {
if len(l.Schema.AllOf) > 0 {
l.Schema = &l.Schema.AllOf[0]
}
if refString := l.Schema.Ref.String(); refString != "" {
str := strings.TrimPrefix(refString, "#/components/schemas/")
sch, ok := l.SchemaList[str]
if ok {
l.Schema = sch
} else {
return fmt.Errorf("unable to resolve %s in OpenAPI V3", refString)
}
}
return nil
}
func (s PatchMetaFromOpenAPIV3) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
l, err := s.traverse(key)
if err != nil {
return l, PatchMeta{}, err
}
p := PatchMeta{}
f, ok := l.Schema.Extensions[patchMergeKey]
if ok {
p.SetPatchMergeKey(f.(string))
}
g, ok := l.Schema.Extensions[patchStrategy]
if ok {
p.SetPatchStrategies(strings.Split(g.(string), ","))
}
err = resolve(&l)
return l, p, err
}
func (s PatchMetaFromOpenAPIV3) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
l, err := s.traverse(key)
if err != nil {
return l, PatchMeta{}, err
}
p := PatchMeta{}
f, ok := l.Schema.Extensions[patchMergeKey]
if ok {
p.SetPatchMergeKey(f.(string))
}
g, ok := l.Schema.Extensions[patchStrategy]
if ok {
p.SetPatchStrategies(strings.Split(g.(string), ","))
}
if l.Schema.Items != nil {
l.Schema = l.Schema.Items.Schema
}
err = resolve(&l)
return l, p, err
}
func (s PatchMetaFromOpenAPIV3) Name() string {
schema := s.Schema
if len(schema.Type) > 0 {
return strings.Join(schema.Type, "")
}
return "Struct"
}
type PatchMetaFromOpenAPI struct { type PatchMetaFromOpenAPI struct {
Schema openapi.Schema Schema openapi.Schema
} }

View File

@ -36,6 +36,9 @@ import (
var ( var (
fakeMergeItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-merge-item.json")} fakeMergeItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-merge-item.json")}
fakePrecisionItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-precision-item.json")} fakePrecisionItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-precision-item.json")}
fakeMergeItemV3Schema = sptest.OpenAPIV3Getter{Path: filepath.Join("testdata", "swagger-merge-item-v3.json")}
fakePrecisionItemV3Schema = sptest.OpenAPIV3Getter{Path: filepath.Join("testdata", "swagger-precision-item-v3.json")}
) )
type SortMergeListTestCases struct { type SortMergeListTestCases struct {
@ -284,9 +287,14 @@ func TestSortMergeLists(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
} }
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{ schemas := []LookupPatchMeta{
mergeItemStructSchema, mergeItemStructSchema,
mergeItemOpenapiSchema, mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
} }
tc := SortMergeListTestCases{} tc := SortMergeListTestCases{}
@ -766,9 +774,14 @@ func TestCustomStrategicMergePatch(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
} }
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{ schemas := []LookupPatchMeta{
mergeItemStructSchema, mergeItemStructSchema,
mergeItemOpenapiSchema, mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
} }
tc := StrategicMergePatchTestCases{} tc := StrategicMergePatchTestCases{}
@ -6169,9 +6182,14 @@ func TestStrategicMergePatch(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
} }
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{ schemas := []LookupPatchMeta{
mergeItemStructSchema, mergeItemStructSchema,
mergeItemOpenapiSchema, mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
} }
tc := StrategicMergePatchTestCases{} tc := StrategicMergePatchTestCases{}
@ -6564,9 +6582,14 @@ func TestNumberConversion(t *testing.T) {
precisionItemOpenapiSchema := PatchMetaFromOpenAPI{ precisionItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakePrecisionItemSchema, "precisionItem"), Schema: sptest.GetSchemaOrDie(&fakePrecisionItemSchema, "precisionItem"),
} }
precisionItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakePrecisionItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakePrecisionItemV3Schema.SchemaOrDie().Components.Schemas["precisionItem"],
}
precisionItemSchemas := []LookupPatchMeta{ precisionItemSchemas := []LookupPatchMeta{
precisionItemStructSchema, precisionItemStructSchema,
precisionItemOpenapiSchema, precisionItemOpenapiSchema,
precisionItemOpenapiV3Schema,
} }
for _, schema := range precisionItemSchemas { for _, schema := range precisionItemSchemas {
@ -6774,9 +6797,14 @@ func TestReplaceWithRawExtension(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
} }
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{ schemas := []LookupPatchMeta{
mergeItemStructSchema, mergeItemStructSchema,
mergeItemOpenapiSchema, mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
} }
for _, schema := range schemas { for _, schema := range schemas {
@ -6946,9 +6974,14 @@ func TestUnknownField(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
} }
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{ schemas := []LookupPatchMeta{
mergeItemStructSchema, mergeItemStructSchema,
mergeItemOpenapiSchema, mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
} }
for _, k := range sets.StringKeySet(testcases).List() { for _, k := range sets.StringKeySet(testcases).List() {

View File

@ -0,0 +1,180 @@
{
"openapi": "3.0",
"info": {
"title": "StrategicMergePatchTestingMergeItem",
"version": "v3.0"
},
"paths": {},
"components": {
"schemas": {
"mergeItem": {
"description": "MergeItem is type definition for testing strategic merge.",
"required": [],
"properties": {
"name": {
"description": "Name field.",
"type": "string"
},
"value": {
"description": "Value field.",
"type": "string"
},
"other": {
"description": "Other field.",
"type": "string"
},
"mergingList": {
"description": "MergingList field.",
"type": "array",
"items": {
"default": {},
"allOf": [
{"$ref": "#/components/schemas/mergeItem"}
]
},
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"nonMergingList": {
"description": "NonMergingList field.",
"type": "array",
"items": {
"$ref": "#/components/schemas/mergeItem"
}
},
"mergingIntList": {
"description": "MergingIntList field.",
"type": "array",
"items": {
"type": "integer",
"format": "int32"
},
"x-kubernetes-patch-strategy": "merge"
},
"nonMergingIntList": {
"description": "NonMergingIntList field.",
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"mergeItemPtr": {
"description": "MergeItemPtr field.",
"allOf": [
{"$ref": "#/components/schemas/mergeItem"}
],
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"simpleMap": {
"description": "SimpleMap field.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"replacingItem": {
"description": "ReplacingItem field.",
"allOf": [
{"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension"}
],
"x-kubernetes-patch-strategy": "replace"
},
"retainKeysMap": {
"description": "RetainKeysMap field.",
"allOf": [
{"$ref": "#/components/schemas/retainKeysMergeItem"}
],
"x-kubernetes-patch-strategy": "retainKeys"
},
"retainKeysMergingList": {
"description": "RetainKeysMergingList field.",
"type": "array",
"items": {
"$ref": "#/components/schemas/mergeItem"
},
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge,retainKeys"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "fake-group",
"kind": "mergeItem",
"version": "some-version"
}
]
},
"retainKeysMergeItem": {
"description": "RetainKeysMergeItem is type definition for testing strategic merge.",
"required": [],
"properties": {
"name": {
"description": "Name field.",
"type": "string"
},
"value": {
"description": "Value field.",
"type": "string"
},
"other": {
"description": "Other field.",
"type": "string"
},
"simpleMap": {
"description": "SimpleMap field.",
"items": {
"type": "string"
}
},
"mergingList": {
"description": "MergingList field.",
"type": "array",
"items": {
"$ref": "#/components/schemas/mergeItem"
},
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"nonMergingList": {
"description": "NonMergingList field.",
"type": "array",
"items": {
"$ref": "#/components/schemas/mergeItem"
}
},
"mergingIntList": {
"description": "MergingIntList field.",
"type": "array",
"items": {
"type": "integer",
"format": "int32"
},
"x-kubernetes-patch-strategy": "merge"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "fake-group",
"kind": "retainKeysMergeItem",
"version": "some-version"
}
]
},
"io.k8s.apimachinery.pkg.runtime.RawExtension": {
"description": "RawExtension is used to hold extensions in external versions.",
"required": [
"Raw"
],
"properties": {
"Raw": {
"description": "Raw is the underlying serialization of this object.",
"type": "string",
"format": "byte"
}
}
}
}
}
}

View File

@ -0,0 +1,49 @@
{
"openapi": "3.0",
"info": {
"title": "StrategicMergePatchTestingPrecisionItem",
"version": "v1.9.0"
},
"paths": {},
"components": {
"schemas": {
"precisionItem": {
"description": "PrecisionItem is type definition for testing strategic merge.",
"required": [],
"properties": {
"name": {
"description": "Name field.",
"type": "string"
},
"int32": {
"description": "Int32 field.",
"type": "integer",
"format": "int32"
},
"int64": {
"description": "Int64 field.",
"type": "integer",
"format": "int64"
},
"float32": {
"description": "Float32 field.",
"type": "number",
"format": "float32"
},
"float64": {
"description": "Float64 field.",
"type": "number",
"format": "float64"
}
},
"x-kubernetes-group-version-kind": [
{
"group": "fake-group",
"kind": "precisionItem",
"version": "some-version"
}
]
}
}
}
}

View File

@ -0,0 +1,65 @@
/*
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 testing
import (
"os"
"sync"
"k8s.io/kube-openapi/pkg/spec3"
)
type OpenAPIV3Getter struct {
Path string
once sync.Once
bytes []byte
openapiv3 spec3.OpenAPI
}
func (f *OpenAPIV3Getter) SchemaBytesOrDie() []byte {
f.once.Do(func() {
_, err := os.Stat(f.Path)
if err != nil {
panic(err)
}
spec, err := os.ReadFile(f.Path)
if err != nil {
panic(err)
}
f.bytes = spec
})
return f.bytes
}
func (f *OpenAPIV3Getter) SchemaOrDie() *spec3.OpenAPI {
f.once.Do(func() {
_, err := os.Stat(f.Path)
if err != nil {
panic(err)
}
spec, err := os.ReadFile(f.Path)
if err != nil {
panic(err)
}
err = f.openapiv3.UnmarshalJSON(spec)
if err != nil {
panic(err)
}
})
return &f.openapiv3
}

View File

@ -39,6 +39,7 @@ import (
"k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/openapi3"
"k8s.io/client-go/util/csaupgrade" "k8s.io/client-go/util/csaupgrade"
"k8s.io/component-base/version" "k8s.io/component-base/version"
"k8s.io/klog/v2" "k8s.io/klog/v2"
@ -105,7 +106,8 @@ type ApplyOptions struct {
Builder *resource.Builder Builder *resource.Builder
Mapper meta.RESTMapper Mapper meta.RESTMapper
DynamicClient dynamic.Interface DynamicClient dynamic.Interface
OpenAPISchema openapi.Resources OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
Namespace string Namespace string
EnforceNamespace bool EnforceNamespace bool
@ -282,7 +284,15 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
return nil, err return nil, err
} }
openAPISchema, _ := f.OpenAPISchema() var openAPIV3Root openapi3.Root
if !cmdutil.OpenAPIV3Patch.IsDisabled() {
openAPIV3Client, err := f.OpenAPIV3Client()
if err == nil {
openAPIV3Root = openapi3.NewRoot(openAPIV3Client)
} else {
klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2")
}
}
validationDirective, err := cmdutil.GetValidationDirective(cmd) validationDirective, err := cmdutil.GetValidationDirective(cmd)
if err != nil { if err != nil {
@ -360,7 +370,8 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
Builder: builder, Builder: builder,
Mapper: mapper, Mapper: mapper,
DynamicClient: dynamicClient, DynamicClient: dynamicClient,
OpenAPISchema: openAPISchema, OpenAPIGetter: f,
OpenAPIV3Root: openAPIV3Root,
IOStreams: flags.IOStreams, IOStreams: flags.IOStreams,

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,9 @@ import (
"k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/openapi3"
"k8s.io/klog/v2"
"k8s.io/kube-openapi/pkg/validation/spec"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util"
@ -50,6 +53,10 @@ const (
backOffPeriod = 1 * time.Second backOffPeriod = 1 * time.Second
// how many times we can retry before back off // how many times we can retry before back off
triesBeforeBackOff = 1 triesBeforeBackOff = 1
// groupVersionKindExtensionKey is the key used to lookup the
// GroupVersionKind value for an object definition from the
// definition's "extensions" map.
groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
) )
var createPatchErrFormat = "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:" var createPatchErrFormat = "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
@ -73,13 +80,17 @@ type Patcher struct {
// Number of retries to make if the patch fails with conflict // Number of retries to make if the patch fails with conflict
Retries int Retries int
OpenapiSchema openapi.Resources OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
} }
func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) { func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) {
var openapiSchema openapi.Resources var openAPIGetter openapi.OpenAPIResourcesGetter
var openAPIV3Root openapi3.Root
if o.OpenAPIPatch { if o.OpenAPIPatch {
openapiSchema = o.OpenAPISchema openAPIGetter = o.OpenAPIGetter
openAPIV3Root = o.OpenAPIV3Root
} }
return &Patcher{ return &Patcher{
@ -91,7 +102,8 @@ func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (
CascadingStrategy: o.DeleteOptions.CascadingStrategy, CascadingStrategy: o.DeleteOptions.CascadingStrategy,
Timeout: o.DeleteOptions.Timeout, Timeout: o.DeleteOptions.Timeout,
GracePeriod: o.DeleteOptions.GracePeriod, GracePeriod: o.DeleteOptions.GracePeriod,
OpenapiSchema: openapiSchema, OpenAPIGetter: openAPIGetter,
OpenAPIV3Root: openAPIV3Root,
Retries: maxPatchRetry, Retries: maxPatchRetry,
}, nil }, nil
} }
@ -118,14 +130,42 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
var patchType types.PatchType var patchType types.PatchType
var patch []byte var patch []byte
if p.OpenapiSchema != nil { if p.OpenAPIV3Root != nil {
// if openapischema is used, we'll try to get required patch type for this GVK from Open API. gvkSupported, err := p.gvkSupportsPatchOpenAPIV3(p.Mapping.GroupVersionKind)
// if it fails or could not find any patch type, fall back to baked-in patch type determination. if err != nil {
if patchType, err = p.getPatchTypeFromOpenAPI(p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType { // Realistically this error logging is not needed (not present in V2),
patch, err = p.buildStrategicMergeFromOpenAPI(original, modified, current) // but would help us in debugging if users encounter a problem
// with OpenAPI V3 not present in V2.
klog.V(5).Infof("warning: OpenAPI V3 path does not exist - group: %s, version %s, kind %s\n",
p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind)
} else if gvkSupported {
patch, err = p.buildStrategicMergePatchFromOpenAPIV3(original, modified, current)
if err != nil { if err != nil {
// Warn user about problem and continue strategic merge patching using builtin types. // Fall back to OpenAPI V2 if there is a problem
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err) // We should remove the fallback in the future,
// but for the first release it might be beneficial
// to fall back to OpenAPI V2 while logging the error
// and seeing if we get any bug reports.
fmt.Fprintf(errOut, "warning: error calculating patch from openapi v3 spec: %v\n", err)
} else {
patchType = types.StrategicMergePatchType
}
} else {
klog.V(5).Infof("warning: OpenAPI V3 path does not support strategic merge patch - group: %s, version %s, kind %s\n",
p.Mapping.GroupVersionKind.Group, p.Mapping.GroupVersionKind.Version, p.Mapping.GroupVersionKind.Kind)
}
}
if patch == nil {
if openAPISchema, err := p.OpenAPIGetter.OpenAPISchema(); err == nil && openAPISchema != nil {
// if openapischema is used, we'll try to get required patch type for this GVK from Open API.
// if it fails or could not find any patch type, fall back to baked-in patch type determination.
if patchType, err = p.getPatchTypeFromOpenAPI(openAPISchema, p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
patch, err = p.buildStrategicMergeFromOpenAPI(openAPISchema, original, modified, current)
if err != nil {
// Warn user about problem and continue strategic merge patching using builtin types.
fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
}
} }
} }
} }
@ -182,10 +222,94 @@ func (p *Patcher) buildMergePatch(original, modified, current []byte) ([]byte, e
return patch, nil return patch, nil
} }
// gvkSupportsPatchOpenAPIV3 checks if a particular GVK supports the patch operation.
// It returns an error if the OpenAPI V3 could not be downloaded.
func (p *Patcher) gvkSupportsPatchOpenAPIV3(gvk schema.GroupVersionKind) (bool, error) {
gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{
Group: p.Mapping.GroupVersionKind.Group,
Version: p.Mapping.GroupVersionKind.Version,
})
if err != nil {
return false, err
}
if gvSpec == nil || gvSpec.Paths == nil || gvSpec.Paths.Paths == nil {
return false, fmt.Errorf("gvk group: %s, version: %s, kind: %s does not exist for OpenAPI V3", gvk.Group, gvk.Version, gvk.Kind)
}
for _, path := range gvSpec.Paths.Paths {
if path.Patch != nil {
if gvkMatchesSingle(p.Mapping.GroupVersionKind, path.Patch.Extensions) {
if path.Patch.RequestBody == nil || path.Patch.RequestBody.Content == nil {
// GVK exists but does not support requestBody. Indication of malformed OpenAPI.
return false, nil
}
if _, ok := path.Patch.RequestBody.Content["application/strategic-merge-patch+json"]; ok {
return true, nil
}
// GVK exists but strategic-merge-patch is not supported. Likely to be a CRD or aggregated resource.
return false, nil
}
}
}
return false, nil
}
func gvkMatchesArray(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool {
var gvkList []map[string]string
err := ext.GetObject(groupVersionKindExtensionKey, &gvkList)
if err != nil {
return false
}
for _, gvkMap := range gvkList {
if gvkMap["group"] == targetGVK.Group &&
gvkMap["version"] == targetGVK.Version &&
gvkMap["kind"] == targetGVK.Kind {
return true
}
}
return false
}
func gvkMatchesSingle(targetGVK schema.GroupVersionKind, ext spec.Extensions) bool {
var gvkMap map[string]string
err := ext.GetObject(groupVersionKindExtensionKey, &gvkMap)
if err != nil {
return false
}
return gvkMap["group"] == targetGVK.Group &&
gvkMap["version"] == targetGVK.Version &&
gvkMap["kind"] == targetGVK.Kind
}
func (p *Patcher) buildStrategicMergePatchFromOpenAPIV3(original, modified, current []byte) ([]byte, error) {
gvSpec, err := p.OpenAPIV3Root.GVSpec(schema.GroupVersion{
Group: p.Mapping.GroupVersionKind.Group,
Version: p.Mapping.GroupVersionKind.Version,
})
if err != nil {
return nil, err
}
if gvSpec == nil || gvSpec.Components == nil {
return nil, fmt.Errorf("OpenAPI V3 Components is nil")
}
for _, c := range gvSpec.Components.Schemas {
if !gvkMatchesArray(p.Mapping.GroupVersionKind, c.Extensions) {
continue
}
lookupPatchMeta := strategicpatch.PatchMetaFromOpenAPIV3{Schema: c, SchemaList: gvSpec.Components.Schemas}
if openapiv3Patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil {
return nil, err
} else {
return openapiv3Patch, nil
}
}
return nil, nil
}
// buildStrategicMergeFromOpenAPI builds patch from OpenAPI if it is enabled. // buildStrategicMergeFromOpenAPI builds patch from OpenAPI if it is enabled.
// This is used for core types which is published in openapi. // This is used for core types which is published in openapi.
func (p *Patcher) buildStrategicMergeFromOpenAPI(original, modified, current []byte) ([]byte, error) { func (p *Patcher) buildStrategicMergeFromOpenAPI(openAPISchema openapi.Resources, original, modified, current []byte) ([]byte, error) {
schema := p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind) schema := openAPISchema.LookupResource(p.Mapping.GroupVersionKind)
if schema == nil { if schema == nil {
// Missing schema returns nil patch; also no error. // Missing schema returns nil patch; also no error.
return nil, nil return nil, nil
@ -199,8 +323,8 @@ func (p *Patcher) buildStrategicMergeFromOpenAPI(original, modified, current []b
} }
// getPatchTypeFromOpenAPI looks up patch types supported by given GroupVersionKind in Open API. // getPatchTypeFromOpenAPI looks up patch types supported by given GroupVersionKind in Open API.
func (p *Patcher) getPatchTypeFromOpenAPI(gvk schema.GroupVersionKind) (types.PatchType, error) { func (p *Patcher) getPatchTypeFromOpenAPI(openAPISchema openapi.Resources, gvk schema.GroupVersionKind) (types.PatchType, error) {
if pc := p.OpenapiSchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil { if pc := openAPISchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil {
for _, c := range pc { for _, c := range pc {
if c == string(types.StrategicMergePatchType) { if c == string(types.StrategicMergePatchType) {
return types.StrategicMergePatchType, nil return types.StrategicMergePatchType, nil

View File

@ -35,6 +35,7 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/openapi3"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/kubectl/pkg/cmd/apply" "k8s.io/kubectl/pkg/cmd/apply"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
@ -109,7 +110,8 @@ type DiffOptions struct {
Concurrency int Concurrency int
Selector string Selector string
OpenAPISchema openapi.Resources OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
DynamicClient dynamic.Interface DynamicClient dynamic.Interface
CmdNamespace string CmdNamespace string
EnforceNamespace bool EnforceNamespace bool
@ -323,7 +325,8 @@ type InfoObject struct {
LocalObj runtime.Object LocalObj runtime.Object
Info *resource.Info Info *resource.Info
Encoder runtime.Encoder Encoder runtime.Encoder
OpenAPI openapi.Resources OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
Force bool Force bool
ServerSideApply bool ServerSideApply bool
FieldManager string FieldManager string
@ -395,7 +398,8 @@ func (obj InfoObject) Merged() (runtime.Object, error) {
Helper: helper, Helper: helper,
Overwrite: true, Overwrite: true,
BackOff: clockwork.NewRealClock(), BackOff: clockwork.NewRealClock(),
OpenapiSchema: obj.OpenAPI, OpenAPIGetter: obj.OpenAPIGetter,
OpenAPIV3Root: obj.OpenAPIV3Root,
ResourceVersion: resourceVersion, ResourceVersion: resourceVersion,
} }
@ -637,9 +641,14 @@ func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
} }
if !o.ServerSideApply { if !o.ServerSideApply {
o.OpenAPISchema, err = f.OpenAPISchema() o.OpenAPIGetter = f
if err != nil { if !cmdutil.OpenAPIV3Patch.IsDisabled() {
return err openAPIV3Client, err := f.OpenAPIV3Client()
if err == nil {
o.OpenAPIV3Root = openapi3.NewRoot(openAPIV3Client)
} else {
klog.V(4).Infof("warning: OpenAPI V3 Patch is enabled but is unable to be loaded. Will fall back to OpenAPI V2")
}
} }
} }
@ -721,7 +730,8 @@ func (o *DiffOptions) Run() error {
LocalObj: local, LocalObj: local,
Info: info, Info: info,
Encoder: scheme.DefaultJSONEncoder(), Encoder: scheme.DefaultJSONEncoder(),
OpenAPI: o.OpenAPISchema, OpenAPIGetter: o.OpenAPIGetter,
OpenAPIV3Root: o.OpenAPIV3Root,
Force: force, Force: force,
ServerSideApply: o.ServerSideApply, ServerSideApply: o.ServerSideApply,
FieldManager: o.FieldManager, FieldManager: o.FieldManager,

View File

@ -195,3 +195,18 @@ func WithAlphaEnvs(features []cmdutil.FeatureGate, t *testing.T, f func(*testing
} }
f(t) f(t)
} }
// WithAlphaEnvs calls func f with the given env-var-based feature gates disabled,
// and then restores the original values of those variables.
func WithAlphaEnvsDisabled(features []cmdutil.FeatureGate, t *testing.T, f func(*testing.T)) {
for _, feature := range features {
key := string(feature)
if key != "" {
oldValue := os.Getenv(key)
err := os.Setenv(key, "false")
require.NoError(t, err, "unexpected error setting alpha env")
defer os.Setenv(key, oldValue)
}
}
f(t)
}

View File

@ -214,5 +214,5 @@ func (f *factoryImpl) OpenAPIV3Client() (openapiclient.Client, error) {
return nil, err return nil, err
} }
return discovery.OpenAPIV3(), nil return cached.NewClient(discovery.OpenAPIV3()), nil
} }

View File

@ -428,6 +428,7 @@ const (
ApplySet FeatureGate = "KUBECTL_APPLYSET" ApplySet FeatureGate = "KUBECTL_APPLYSET"
CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW" CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW"
InteractiveDelete FeatureGate = "KUBECTL_INTERACTIVE_DELETE" InteractiveDelete FeatureGate = "KUBECTL_INTERACTIVE_DELETE"
OpenAPIV3Patch FeatureGate = "KUBECTL_OPENAPIV3_PATCH"
RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS" RemoteCommandWebsockets FeatureGate = "KUBECTL_REMOTE_COMMAND_WEBSOCKETS"
) )

File diff suppressed because it is too large Load Diff