Use OpenAPI V3 for client side SMP

This commit is contained in:
Jefftree 2023-09-15 16:46:53 -04:00
parent 74fefd877f
commit 4f3b0b1518
9 changed files with 16252 additions and 2 deletions

View File

@ -20,12 +20,17 @@ import (
"errors"
"fmt"
"reflect"
"strings"
"k8s.io/apimachinery/pkg/util/mergepatch"
forkedjson "k8s.io/apimachinery/third_party/forked/golang/json"
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 {
patchStrategies []string
patchMergeKey string
@ -148,6 +153,90 @@ func GetTagStructTypeOrDie(dataStruct interface{}) reflect.Type {
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
}
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 {
Schema openapi.Schema
}

View File

@ -36,6 +36,9 @@ import (
var (
fakeMergeItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-merge-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 {
@ -284,9 +287,14 @@ func TestSortMergeLists(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
}
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
}
tc := SortMergeListTestCases{}
@ -766,9 +774,14 @@ func TestCustomStrategicMergePatch(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
}
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
}
tc := StrategicMergePatchTestCases{}
@ -6169,9 +6182,14 @@ func TestStrategicMergePatch(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
}
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
}
tc := StrategicMergePatchTestCases{}
@ -6564,9 +6582,14 @@ func TestNumberConversion(t *testing.T) {
precisionItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakePrecisionItemSchema, "precisionItem"),
}
precisionItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakePrecisionItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakePrecisionItemV3Schema.SchemaOrDie().Components.Schemas["precisionItem"],
}
precisionItemSchemas := []LookupPatchMeta{
precisionItemStructSchema,
precisionItemOpenapiSchema,
precisionItemOpenapiV3Schema,
}
for _, schema := range precisionItemSchemas {
@ -6774,9 +6797,14 @@ func TestReplaceWithRawExtension(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
}
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
}
for _, schema := range schemas {
@ -6946,9 +6974,14 @@ func TestUnknownField(t *testing.T) {
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"),
}
mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{
SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas,
Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"],
}
schemas := []LookupPatchMeta{
mergeItemStructSchema,
mergeItemOpenapiSchema,
mergeItemOpenapiV3Schema,
}
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,8 @@ import (
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
cachedopenapi "k8s.io/client-go/openapi/cached"
"k8s.io/client-go/openapi3"
"k8s.io/client-go/util/csaupgrade"
"k8s.io/component-base/version"
"k8s.io/klog/v2"
@ -106,6 +108,7 @@ type ApplyOptions struct {
Mapper meta.RESTMapper
DynamicClient dynamic.Interface
OpenAPISchema openapi.Resources
OpenAPIV3Root openapi3.Root
Namespace string
EnforceNamespace bool
@ -283,6 +286,12 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
}
openAPISchema, _ := f.OpenAPISchema()
var openAPIV3Root openapi3.Root
openAPIV3Client, err := f.OpenAPIV3Client()
if err == nil {
cachedOpenAPIV3Client := cachedopenapi.NewClient(openAPIV3Client)
openAPIV3Root = openapi3.NewRoot(cachedOpenAPIV3Client)
}
validationDirective, err := cmdutil.GetValidationDirective(cmd)
if err != nil {
@ -361,6 +370,7 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
Mapper: mapper,
DynamicClient: dynamicClient,
OpenAPISchema: openAPISchema,
OpenAPIV3Root: openAPIV3Root,
IOStreams: flags.IOStreams,

View File

@ -47,6 +47,8 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource"
dynamicfakeclient "k8s.io/client-go/dynamic/fake"
openapiclient "k8s.io/client-go/openapi"
"k8s.io/client-go/openapi/openapitest"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake"
testing2 "k8s.io/client-go/testing"
@ -64,11 +66,16 @@ import (
var (
fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "swagger.json")}
fakeOpenAPIV3Legacy = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "api", "v1.json")}
fakeOpenAPIV3AppsV1 = sptest.OpenAPIV3Getter{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "v3", "apis", "apps", "v1.json")}
testingOpenAPISchemas = []testOpenAPISchema{AlwaysErrorsOpenAPISchema, FakeOpenAPISchema}
AlwaysErrorsOpenAPISchema = testOpenAPISchema{
OpenAPISchemaFn: func() (openapi.Resources, error) {
return nil, errors.New("cannot get openapi spec")
},
OpenAPIV3ClientFunc: func() (openapiclient.Client, error) {
return nil, errors.New("cannot get openapiv3 client")
},
}
FakeOpenAPISchema = testOpenAPISchema{
OpenAPISchemaFn: func() (openapi.Resources, error) {
@ -78,12 +85,19 @@ var (
}
return openapi.NewOpenAPIData(s)
},
OpenAPIV3ClientFunc: func() (openapiclient.Client, error) {
c := openapitest.NewFakeClient()
c.PathsMap["api/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3Legacy.SchemaBytesOrDie()}
c.PathsMap["apis/apps/v1"] = openapitest.FakeGroupVersion{GVSpec: fakeOpenAPIV3AppsV1.SchemaBytesOrDie()}
return c, nil
},
}
codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
)
type testOpenAPISchema struct {
OpenAPISchemaFn func() (openapi.Resources, error)
OpenAPISchemaFn func() (openapi.Resources, error)
OpenAPIV3ClientFunc func() (openapiclient.Client, error)
}
func TestApplyExtraArgsFail(t *testing.T) {
@ -684,6 +698,7 @@ func TestApplyObjectWithoutAnnotation(t *testing.T) {
}),
}
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -730,6 +745,7 @@ func TestApplyObject(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -778,6 +794,7 @@ func TestApplyPruneObjects(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1014,6 +1031,7 @@ func TestApplyPruneObjectsWithAllowlist(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
for _, resource := range tc.currentResources {
@ -1192,6 +1210,7 @@ func TestApplyCSAMigration(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1280,6 +1299,7 @@ func TestApplyObjectOutput(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1341,6 +1361,7 @@ func TestApplyRetry(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1516,6 +1537,7 @@ func testApplyMultipleObjects(t *testing.T, asList bool) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1611,6 +1633,7 @@ func TestApplyNULLPreservation(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1673,6 +1696,7 @@ func TestUnstructuredApply(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1737,6 +1761,7 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1979,6 +2004,7 @@ func TestForceApply(t *testing.T) {
fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
tf.FakeDynamicClient = fakeDynamicClient
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.Client = tf.UnstructuredClient
tf.ClientConfigVal = &restclient.Config{}
@ -2830,6 +2856,7 @@ func TestApplyWithPruneV2(t *testing.T) {
}
tf.Client = tf.UnstructuredClient
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
manifests := []string{"manifest1", "manifest2"}
@ -3104,6 +3131,7 @@ func TestApplyWithPruneV2Fail(t *testing.T) {
}
tf.Client = tf.UnstructuredClient
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
testdirs := []string{"testdata/prune/simple"}
for _, testdir := range testdirs {

View File

@ -37,6 +37,9 @@ import (
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/wait"
"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"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util"
@ -50,6 +53,10 @@ const (
backOffPeriod = 1 * time.Second
// how many times we can retry before back off
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:"
@ -74,6 +81,7 @@ type Patcher struct {
Retries int
OpenapiSchema openapi.Resources
OpenAPIV3Root openapi3.Root
}
func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (*Patcher, error) {
@ -92,6 +100,7 @@ func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (
Timeout: o.DeleteOptions.Timeout,
GracePeriod: o.DeleteOptions.GracePeriod,
OpenapiSchema: openapiSchema,
OpenAPIV3Root: o.OpenAPIV3Root,
Retries: maxPatchRetry,
}, nil
}
@ -118,7 +127,35 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, namespace, na
var patchType types.PatchType
var patch []byte
if p.OpenapiSchema != nil {
if p.OpenAPIV3Root != nil {
gvkSupported, err := p.gvkSupportsPatchOpenAPIV3(p.Mapping.GroupVersionKind)
if err != nil {
// Realistically this error logging is not needed (not present in V2),
// 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 {
// Fall back to OpenAPI V2 if there is a problem
// 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 && p.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(p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
@ -182,6 +219,90 @@ func (p *Patcher) buildMergePatch(original, modified, current []byte) ([]byte, e
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.
// This is used for core types which is published in openapi.
func (p *Patcher) buildStrategicMergeFromOpenAPI(original, modified, current []byte) ([]byte, error) {

File diff suppressed because it is too large Load Diff