Merge pull request #120707 from Jefftree/csa-openapiv3
Use OpenAPI V3 for client side SMP
This commit is contained in:
		| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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() { | ||||||
|   | |||||||
							
								
								
									
										180
									
								
								staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-merge-item-v3.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-merge-item-v3.json
									
									
									
									
										vendored
									
									
										Normal 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" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-precision-item-v3.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-precision-item-v3.json
									
									
									
									
										vendored
									
									
										Normal 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" | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  | } | ||||||
| @@ -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, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										15675
									
								
								staging/src/k8s.io/kubectl/testdata/openapi/v3/apis/apps/v1.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15675
									
								
								staging/src/k8s.io/kubectl/testdata/openapi/v3/apis/apps/v1.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot