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,

View File

@ -47,6 +47,8 @@ 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"
dynamicfakeclient "k8s.io/client-go/dynamic/fake" 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" restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake" "k8s.io/client-go/rest/fake"
testing2 "k8s.io/client-go/testing" testing2 "k8s.io/client-go/testing"
@ -64,11 +66,17 @@ import (
var ( var (
fakeSchema = sptest.Fake{Path: filepath.Join("..", "..", "..", "testdata", "openapi", "swagger.json")} 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} testingOpenAPISchemas = []testOpenAPISchema{AlwaysErrorsOpenAPISchema, FakeOpenAPISchema}
AlwaysErrorsOpenAPISchema = testOpenAPISchema{ AlwaysErrorsOpenAPISchema = testOpenAPISchema{
OpenAPISchemaFn: func() (openapi.Resources, error) { OpenAPISchemaFn: func() (openapi.Resources, error) {
return nil, errors.New("cannot get openapi spec") return nil, errors.New("cannot get openapi spec")
}, },
OpenAPIV3ClientFunc: func() (openapiclient.Client, error) {
return nil, errors.New("cannot get openapiv3 client")
},
} }
FakeOpenAPISchema = testOpenAPISchema{ FakeOpenAPISchema = testOpenAPISchema{
OpenAPISchemaFn: func() (openapi.Resources, error) { OpenAPISchemaFn: func() (openapi.Resources, error) {
@ -78,12 +86,43 @@ var (
} }
return openapi.NewOpenAPIData(s) 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
},
} }
AlwaysPanicSchema = testOpenAPISchema{
OpenAPISchemaFn: func() (openapi.Resources, error) {
panic("error, openAPIV2 should not be called")
},
OpenAPIV3ClientFunc: func() (openapiclient.Client, error) {
return &OpenAPIV3ClientAlwaysPanic{}, nil
},
}
codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
) )
type OpenAPIV3ClientAlwaysPanic struct{}
func (o *OpenAPIV3ClientAlwaysPanic) Paths() (map[string]openapiclient.GroupVersion, error) {
panic("Cannot get paths")
}
func noopOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) {
f(t)
}
func disableOpenAPIV3Patch(t *testing.T, f func(t *testing.T)) {
cmdtesting.WithAlphaEnvsDisabled([]cmdutil.FeatureGate{cmdutil.OpenAPIV3Patch}, t, f)
}
var applyFeatureToggles = []func(*testing.T, func(t *testing.T)){noopOpenAPIV3Patch, disableOpenAPIV3Patch}
type testOpenAPISchema struct { type testOpenAPISchema struct {
OpenAPISchemaFn func() (openapi.Resources, error) OpenAPISchemaFn func() (openapi.Resources, error)
OpenAPIV3ClientFunc func() (openapiclient.Client, error)
} }
func TestApplyExtraArgsFail(t *testing.T) { func TestApplyExtraArgsFail(t *testing.T) {
@ -684,6 +723,7 @@ func TestApplyObjectWithoutAnnotation(t *testing.T) {
}), }),
} }
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams) cmd := NewCmdApply("kubectl", tf, ioStreams)
@ -702,13 +742,17 @@ func TestApplyObjectWithoutAnnotation(t *testing.T) {
} }
} }
func TestApplyObject(t *testing.T) { func TestOpenAPIV3PatchFeatureFlag(t *testing.T) {
// OpenAPIV3 smp apply is on by default.
// Test that users can disable it to use OpenAPI V2 smp
// An OpenAPI V3 root that always panics is used to ensure
// the v3 code path is never exercised when the feature is disabled
cmdtesting.InitTestErrorHandler(t) cmdtesting.InitTestErrorHandler(t)
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC) nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, testingOpenAPISchema := range testingOpenAPISchemas { t.Run("test apply when a local object is specified - openapi v2 smp", func(t *testing.T) {
t.Run("test apply when a local object is specified", func(t *testing.T) { disableOpenAPIV3Patch(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -729,7 +773,8 @@ func TestApplyObject(t *testing.T) {
} }
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = AlwaysPanicSchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -747,6 +792,109 @@ func TestApplyObject(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String()) t.Fatalf("unexpected error output: %s", errBuf.String())
} }
}) })
})
}
func TestOpenAPIV3DoesNotLoadV2(t *testing.T) {
cmdtesting.InitTestErrorHandler(t)
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := io.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req, types.StrategicMergePatchType)
bodyRC := io.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.OpenAPISchemaFunc = AlwaysPanicSchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
// uses the name from the file, not the response
expectRC := "replicationcontroller/" + nameRC + "\n"
if buf.String() != expectRC {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
})
}
func TestApplyObject(t *testing.T) {
cmdtesting.InitTestErrorHandler(t)
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply when a local object is specified - openapi v3 smp", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
switch p, m := req.URL.Path, req.Method; {
case p == pathRC && m == "GET":
bodyRC := io.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
case p == pathRC && m == "PATCH":
validatePatchApplication(t, req, types.StrategicMergePatchType)
bodyRC := io.NopCloser(bytes.NewReader(currentRC))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
cmd := NewCmdApply("kubectl", tf, ioStreams)
cmd.Flags().Set("filename", filenameRC)
cmd.Flags().Set("output", "name")
cmd.Run(cmd, []string{})
// uses the name from the file, not the response
expectRC := "replicationcontroller/" + nameRC + "\n"
if buf.String() != expectRC {
t.Fatalf("unexpected output: %s\nexpected: %s", buf.String(), expectRC)
}
if errBuf.String() != "" {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
})
})
}
} }
} }
@ -756,7 +904,10 @@ func TestApplyPruneObjects(t *testing.T) {
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply returns correct output", func(t *testing.T) { t.Run("test apply returns correct output", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -778,6 +929,7 @@ func TestApplyPruneObjects(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -796,6 +948,8 @@ func TestApplyPruneObjects(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String()) t.Fatalf("unexpected error output: %s", errBuf.String())
} }
}) })
})
}
} }
} }
@ -1014,6 +1168,7 @@ func TestApplyPruneObjectsWithAllowlist(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
for _, resource := range tc.currentResources { for _, resource := range tc.currentResources {
@ -1093,6 +1248,8 @@ func TestApplyCSAMigration(t *testing.T) {
nameRC, rcWithManagedFields := readAndAnnotateReplicationController(t, filenameRCManagedFieldsLA) nameRC, rcWithManagedFields := readAndAnnotateReplicationController(t, filenameRCManagedFieldsLA)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, openAPIFeatureToggle := range applyFeatureToggles {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -1192,6 +1349,7 @@ func TestApplyCSAMigration(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1234,6 +1392,8 @@ func TestApplyCSAMigration(t *testing.T) {
require.Empty(t, errBuf) require.Empty(t, errBuf)
require.Equal(t, 4, applies, "only a single call to server-side apply should have been performed") require.Equal(t, 4, applies, "only a single call to server-side apply should have been performed")
require.Equal(t, targetPatches, patches, "no more json patches should have been needed") require.Equal(t, targetPatches, patches, "no more json patches should have been needed")
})
}
} }
func TestApplyObjectOutput(t *testing.T) { func TestApplyObjectOutput(t *testing.T) {
@ -1258,7 +1418,9 @@ func TestApplyObjectOutput(t *testing.T) {
} }
for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply returns correct output", func(t *testing.T) { t.Run("test apply returns correct output", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -1280,6 +1442,7 @@ func TestApplyObjectOutput(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1298,6 +1461,8 @@ func TestApplyObjectOutput(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String()) t.Fatalf("unexpected error output: %s", errBuf.String())
} }
}) })
})
}
} }
} }
@ -1307,7 +1472,10 @@ func TestApplyRetry(t *testing.T) {
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply retries on conflict error", func(t *testing.T) { t.Run("test apply retries on conflict error", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
firstPatch := true firstPatch := true
retry := false retry := false
getCount := 0 getCount := 0
@ -1341,6 +1509,7 @@ func TestApplyRetry(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1362,6 +1531,8 @@ func TestApplyRetry(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String()) t.Fatalf("unexpected error output: %s", errBuf.String())
} }
}) })
})
}
} }
} }
@ -1516,6 +1687,7 @@ func testApplyMultipleObjects(t *testing.T, asList bool) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1568,7 +1740,10 @@ func TestApplyNULLPreservation(t *testing.T) {
deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside) deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside)
for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply preserves NULL fields", func(t *testing.T) { t.Run("test apply preserves NULL fields", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -1611,6 +1786,7 @@ func TestApplyNULLPreservation(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1631,6 +1807,8 @@ func TestApplyNULLPreservation(t *testing.T) {
t.Fatal("No server-side patch call detected") t.Fatal("No server-side patch call detected")
} }
}) })
})
}
} }
} }
@ -1643,7 +1821,10 @@ func TestUnstructuredApply(t *testing.T) {
verifiedPatch := false verifiedPatch := false
for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply works correctly with unstructured objects", func(t *testing.T) { t.Run("test apply works correctly with unstructured objects", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -1673,6 +1854,7 @@ func TestUnstructuredApply(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1692,6 +1874,8 @@ func TestUnstructuredApply(t *testing.T) {
t.Fatal("No server-side patch call detected") t.Fatal("No server-side patch call detected")
} }
}) })
})
}
} }
} }
@ -1707,7 +1891,11 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
path := "/namespaces/test/widgets/widget" path := "/namespaces/test/widgets/widget"
for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) { t.Run("test repeated apply operations on an unstructured object", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test") tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup() defer tf.Cleanup()
@ -1737,6 +1925,7 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
}), }),
} }
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig() tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams() ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1753,6 +1942,8 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String()) t.Fatalf("unexpected error output: %s", errBuf.String())
} }
}) })
})
}
} }
} }
@ -1897,7 +2088,10 @@ func TestForceApply(t *testing.T) {
} }
for _, testingOpenAPISchema := range testingOpenAPISchemas { for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply with --force", func(t *testing.T) { t.Run("test apply with --force", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
deleted := false deleted := false
isScaledDownToZero := false isScaledDownToZero := false
counts := map[string]int{} counts := map[string]int{}
@ -1979,6 +2173,7 @@ func TestForceApply(t *testing.T) {
fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme) fakeDynamicClient := dynamicfakeclient.NewSimpleDynamicClient(scheme)
tf.FakeDynamicClient = fakeDynamicClient tf.FakeDynamicClient = fakeDynamicClient
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.Client = tf.UnstructuredClient tf.Client = tf.UnstructuredClient
tf.ClientConfigVal = &restclient.Config{} tf.ClientConfigVal = &restclient.Config{}
@ -2002,6 +2197,8 @@ func TestForceApply(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String()) t.Fatalf("unexpected error output: %s", errBuf.String())
} }
}) })
})
}
} }
} }
@ -2830,6 +3027,7 @@ func TestApplyWithPruneV2(t *testing.T) {
} }
tf.Client = tf.UnstructuredClient tf.Client = tf.UnstructuredClient
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) { cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.ApplySet}, t, func(t *testing.T) {
manifests := []string{"manifest1", "manifest2"} manifests := []string{"manifest1", "manifest2"}
@ -3104,6 +3302,7 @@ func TestApplyWithPruneV2Fail(t *testing.T) {
} }
tf.Client = tf.UnstructuredClient tf.Client = tf.UnstructuredClient
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
testdirs := []string{"testdata/prune/simple"} testdirs := []string{"testdata/prune/simple"}
for _, testdir := range testdirs { for _, testdir := range testdirs {

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,17 +130,45 @@ 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 {
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 {
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 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 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 { if patchType, err = p.getPatchTypeFromOpenAPI(openAPISchema, p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
patch, err = p.buildStrategicMergeFromOpenAPI(original, modified, current) patch, err = p.buildStrategicMergeFromOpenAPI(openAPISchema, original, modified, current)
if err != nil { if err != nil {
// Warn user about problem and continue strategic merge patching using builtin types. // 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) fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
} }
} }
} }
}
if patch == nil { if patch == nil {
versionedObj, err := scheme.Scheme.New(p.Mapping.GroupVersionKind) versionedObj, err := scheme.Scheme.New(p.Mapping.GroupVersionKind)
@ -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