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"
"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
}
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 {
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,7 @@ import (
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/openapi3"
"k8s.io/client-go/util/csaupgrade"
"k8s.io/component-base/version"
"k8s.io/klog/v2"
@ -105,7 +106,8 @@ type ApplyOptions struct {
Builder *resource.Builder
Mapper meta.RESTMapper
DynamicClient dynamic.Interface
OpenAPISchema openapi.Resources
OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
Namespace string
EnforceNamespace bool
@ -282,7 +284,15 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
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)
if err != nil {
@ -360,7 +370,8 @@ func (flags *ApplyFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, baseNa
Builder: builder,
Mapper: mapper,
DynamicClient: dynamicClient,
OpenAPISchema: openAPISchema,
OpenAPIGetter: f,
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,17 @@ 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 +86,43 @@ 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
},
}
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()...)
)
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 {
OpenAPISchemaFn func() (openapi.Resources, error)
OpenAPIV3ClientFunc func() (openapiclient.Client, error)
}
func TestApplyExtraArgsFail(t *testing.T) {
@ -684,6 +723,7 @@ func TestApplyObjectWithoutAnnotation(t *testing.T) {
}),
}
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
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)
nameRC, currentRC := readAndAnnotateReplicationController(t, filenameRC)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, testingOpenAPISchema := range testingOpenAPISchemas {
t.Run("test apply when a local object is specified", func(t *testing.T) {
t.Run("test apply when a local object is specified - openapi v2 smp", func(t *testing.T) {
disableOpenAPIV3Patch(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
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()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -747,6 +792,109 @@ func TestApplyObject(t *testing.T) {
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
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply returns correct output", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
@ -778,6 +929,7 @@ func TestApplyPruneObjects(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -796,6 +948,8 @@ func TestApplyPruneObjects(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
})
})
}
}
}
@ -1014,6 +1168,7 @@ func TestApplyPruneObjectsWithAllowlist(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
for _, resource := range tc.currentResources {
@ -1093,6 +1248,8 @@ func TestApplyCSAMigration(t *testing.T) {
nameRC, rcWithManagedFields := readAndAnnotateReplicationController(t, filenameRCManagedFieldsLA)
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, openAPIFeatureToggle := range applyFeatureToggles {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
@ -1192,6 +1349,7 @@ func TestApplyCSAMigration(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = FakeOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = FakeOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1234,6 +1392,8 @@ func TestApplyCSAMigration(t *testing.T) {
require.Empty(t, errBuf)
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")
})
}
}
func TestApplyObjectOutput(t *testing.T) {
@ -1258,7 +1418,9 @@ func TestApplyObjectOutput(t *testing.T) {
}
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply returns correct output", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
@ -1280,6 +1442,7 @@ func TestApplyObjectOutput(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1298,6 +1461,8 @@ func TestApplyObjectOutput(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
})
})
}
}
}
@ -1307,7 +1472,10 @@ func TestApplyRetry(t *testing.T) {
pathRC := "/namespaces/test/replicationcontrollers/" + nameRC
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply retries on conflict error", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
firstPatch := true
retry := false
getCount := 0
@ -1341,6 +1509,7 @@ func TestApplyRetry(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1362,6 +1531,8 @@ func TestApplyRetry(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
})
})
}
}
}
@ -1516,6 +1687,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()
@ -1568,7 +1740,10 @@ func TestApplyNULLPreservation(t *testing.T) {
deploymentBytes := readDeploymentFromFile(t, filenameDeployObjServerside)
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply preserves NULL fields", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
@ -1611,6 +1786,7 @@ func TestApplyNULLPreservation(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1631,6 +1807,8 @@ func TestApplyNULLPreservation(t *testing.T) {
t.Fatal("No server-side patch call detected")
}
})
})
}
}
}
@ -1643,7 +1821,10 @@ func TestUnstructuredApply(t *testing.T) {
verifiedPatch := false
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply works correctly with unstructured objects", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("test")
defer tf.Cleanup()
@ -1673,6 +1854,7 @@ func TestUnstructuredApply(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1692,6 +1874,8 @@ func TestUnstructuredApply(t *testing.T) {
t.Fatal("No server-side patch call detected")
}
})
})
}
}
}
@ -1707,7 +1891,11 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
path := "/namespaces/test/widgets/widget"
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
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")
defer tf.Cleanup()
@ -1737,6 +1925,7 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
}),
}
tf.OpenAPISchemaFunc = testingOpenAPISchema.OpenAPISchemaFn
tf.OpenAPIV3ClientFunc = testingOpenAPISchema.OpenAPIV3ClientFunc
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
ioStreams, _, buf, errBuf := genericiooptions.NewTestIOStreams()
@ -1753,6 +1942,8 @@ func TestUnstructuredIdempotentApply(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
})
})
}
}
}
@ -1897,7 +2088,10 @@ func TestForceApply(t *testing.T) {
}
for _, testingOpenAPISchema := range testingOpenAPISchemas {
for _, openAPIFeatureToggle := range applyFeatureToggles {
t.Run("test apply with --force", func(t *testing.T) {
openAPIFeatureToggle(t, func(t *testing.T) {
deleted := false
isScaledDownToZero := false
counts := map[string]int{}
@ -1979,6 +2173,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{}
@ -2002,6 +2197,8 @@ func TestForceApply(t *testing.T) {
t.Fatalf("unexpected error output: %s", errBuf.String())
}
})
})
}
}
}
@ -2830,6 +3027,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 +3302,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:"
@ -73,13 +80,17 @@ type Patcher struct {
// Number of retries to make if the patch fails with conflict
Retries int
OpenapiSchema openapi.Resources
OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
}
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 {
openapiSchema = o.OpenAPISchema
openAPIGetter = o.OpenAPIGetter
openAPIV3Root = o.OpenAPIV3Root
}
return &Patcher{
@ -91,7 +102,8 @@ func newPatcher(o *ApplyOptions, info *resource.Info, helper *resource.Helper) (
CascadingStrategy: o.DeleteOptions.CascadingStrategy,
Timeout: o.DeleteOptions.Timeout,
GracePeriod: o.DeleteOptions.GracePeriod,
OpenapiSchema: openapiSchema,
OpenAPIGetter: openAPIGetter,
OpenAPIV3Root: openAPIV3Root,
Retries: maxPatchRetry,
}, nil
}
@ -118,17 +130,45 @@ 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 {
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(p.Mapping.GroupVersionKind); err == nil && patchType == types.StrategicMergePatchType {
patch, err = p.buildStrategicMergeFromOpenAPI(original, modified, current)
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)
}
}
}
}
if patch == nil {
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
}
// 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) {
schema := p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind)
func (p *Patcher) buildStrategicMergeFromOpenAPI(openAPISchema openapi.Resources, original, modified, current []byte) ([]byte, error) {
schema := openAPISchema.LookupResource(p.Mapping.GroupVersionKind)
if schema == nil {
// Missing schema returns nil patch; also no error.
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.
func (p *Patcher) getPatchTypeFromOpenAPI(gvk schema.GroupVersionKind) (types.PatchType, error) {
if pc := p.OpenapiSchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil {
func (p *Patcher) getPatchTypeFromOpenAPI(openAPISchema openapi.Resources, gvk schema.GroupVersionKind) (types.PatchType, error) {
if pc := openAPISchema.GetConsumes(p.Mapping.GroupVersionKind, "PATCH"); pc != nil {
for _, c := range pc {
if c == string(types.StrategicMergePatchType) {
return types.StrategicMergePatchType, nil

View File

@ -35,6 +35,7 @@ import (
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/openapi3"
"k8s.io/klog/v2"
"k8s.io/kubectl/pkg/cmd/apply"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
@ -109,7 +110,8 @@ type DiffOptions struct {
Concurrency int
Selector string
OpenAPISchema openapi.Resources
OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
DynamicClient dynamic.Interface
CmdNamespace string
EnforceNamespace bool
@ -323,7 +325,8 @@ type InfoObject struct {
LocalObj runtime.Object
Info *resource.Info
Encoder runtime.Encoder
OpenAPI openapi.Resources
OpenAPIGetter openapi.OpenAPIResourcesGetter
OpenAPIV3Root openapi3.Root
Force bool
ServerSideApply bool
FieldManager string
@ -395,7 +398,8 @@ func (obj InfoObject) Merged() (runtime.Object, error) {
Helper: helper,
Overwrite: true,
BackOff: clockwork.NewRealClock(),
OpenapiSchema: obj.OpenAPI,
OpenAPIGetter: obj.OpenAPIGetter,
OpenAPIV3Root: obj.OpenAPIV3Root,
ResourceVersion: resourceVersion,
}
@ -637,9 +641,14 @@ func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
}
if !o.ServerSideApply {
o.OpenAPISchema, err = f.OpenAPISchema()
if err != nil {
return err
o.OpenAPIGetter = f
if !cmdutil.OpenAPIV3Patch.IsDisabled() {
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,
Info: info,
Encoder: scheme.DefaultJSONEncoder(),
OpenAPI: o.OpenAPISchema,
OpenAPIGetter: o.OpenAPIGetter,
OpenAPIV3Root: o.OpenAPIV3Root,
Force: force,
ServerSideApply: o.ServerSideApply,
FieldManager: o.FieldManager,

View File

@ -195,3 +195,18 @@ func WithAlphaEnvs(features []cmdutil.FeatureGate, t *testing.T, f func(*testing
}
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 discovery.OpenAPIV3(), nil
return cached.NewClient(discovery.OpenAPIV3()), nil
}

View File

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

File diff suppressed because it is too large Load Diff