diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index 303b5fea97..0c6bf585a9 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -400,8 +400,8 @@ func AddDryRunFlag(cmd *cobra.Command) { } func AddServerSideApplyFlags(cmd *cobra.Command) { - cmd.Flags().Bool("server-side", false, "If true, apply runs in the server instead of the client.") - cmd.Flags().Bool("force-conflicts", false, "If true, server-side apply will force the changes against conflicts.") + cmd.Flags().Bool("server-side", false, "If true, apply runs in the server instead of the client. This is an alpha feature and flag.") + cmd.Flags().Bool("force-conflicts", false, "If true, server-side apply will force the changes against conflicts. This is an alpha feature and flag.") } func AddIncludeUninitializedFlag(cmd *cobra.Command) { diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 5a48afa740..263d04c6ba 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -3451,6 +3451,9 @@ type ServiceSpec struct { // More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies // +patchMergeKey=port // +patchStrategy=merge + // +listType=map + // +listMapKey=port + // +listMapKey=protocol Ports []ServicePort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"port" protobuf:"bytes,1,rep,name=ports"` // Route service traffic to pods with label keys and values matching this diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD index 5da6c14cda..1dbe1d792c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD @@ -55,11 +55,14 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/version:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library", diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go index b6830d762c..09dc617ea1 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go @@ -21,6 +21,18 @@ import ( "net/http" "time" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + _ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset" + _ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions" + _ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion" + internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion" + "k8s.io/apiextensions-apiserver/pkg/controller/establish" + "k8s.io/apiextensions-apiserver/pkg/controller/finalizer" + "k8s.io/apiextensions-apiserver/pkg/controller/status" + "k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -32,20 +44,6 @@ import ( genericapiserver "k8s.io/apiserver/pkg/server" serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/apiserver/pkg/util/webhook" - - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset" - internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion" - "k8s.io/apiextensions-apiserver/pkg/controller/establish" - "k8s.io/apiextensions-apiserver/pkg/controller/finalizer" - "k8s.io/apiextensions-apiserver/pkg/controller/status" - "k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition" - - _ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - _ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions" - _ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion" ) var ( @@ -184,6 +182,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) c.ExtraConfig.ServiceResolver, c.ExtraConfig.AuthResolverWrapper, c.ExtraConfig.MasterCount, + s.GenericAPIServer.Authorizer, ) if err != nil { return nil, err diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index 264a794b36..b26859e03e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -28,8 +28,17 @@ import ( "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/validate" - "k8s.io/klog" - + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion" + apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" + informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion" + listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion" + "k8s.io/apiextensions-apiserver/pkg/controller/establish" + "k8s.io/apiextensions-apiserver/pkg/controller/finalizer" + "k8s.io/apiextensions-apiserver/pkg/crdserverscheme" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" + "k8s.io/apiextensions-apiserver/pkg/registry/customresource" + "k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor" apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -44,30 +53,22 @@ import ( "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/handlers" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/metrics" apirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/generic" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/storage/storagebackend" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/scale" "k8s.io/client-go/scale/scheme/autoscalingv1" "k8s.io/client-go/tools/cache" - - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" - "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion" - apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" - informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion" - listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion" - "k8s.io/apiextensions-apiserver/pkg/controller/establish" - "k8s.io/apiextensions-apiserver/pkg/controller/finalizer" - "k8s.io/apiextensions-apiserver/pkg/crdserverscheme" - apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" - "k8s.io/apiextensions-apiserver/pkg/registry/customresource" - "k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor" - "k8s.io/apiserver/pkg/util/webhook" + "k8s.io/klog" ) // crdHandler serves the `/apis` endpoint. @@ -96,6 +97,9 @@ type crdHandler struct { masterCount int converterFactory *conversion.CRConverterFactory + + // so that we can do create on update. + authorizer authorizer.Authorizer } // crdInfo stores enough information to serve the storage for the custom resource @@ -134,7 +138,8 @@ func NewCustomResourceDefinitionHandler( establishingController *establish.EstablishingController, serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper, - masterCount int) (*crdHandler, error) { + masterCount int, + authorizer authorizer.Authorizer) (*crdHandler, error) { ret := &crdHandler{ versionDiscoveryHandler: versionDiscoveryHandler, groupDiscoveryHandler: groupDiscoveryHandler, @@ -145,6 +150,7 @@ func NewCustomResourceDefinitionHandler( admission: admission, establishingController: establishingController, masterCount: masterCount, + authorizer: authorizer, } crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ UpdateFunc: ret.updateCustomResourceDefinition, @@ -228,6 +234,9 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { string(types.JSONPatchType), string(types.MergePatchType), } + if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) { + supportedTypes = append(supportedTypes, string(types.ApplyPatchType)) + } var handler http.HandlerFunc subresources, err := getSubresourcesForVersion(crd, requestInfo.APIVersion) @@ -565,6 +574,18 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource MetaGroupVersion: metav1.SchemeGroupVersion, TableConvertor: storages[v.Name].CustomResource, + + Authorizer: r.authorizer, + } + if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) { + reqScope := requestScopes[v.Name] + reqScope.FieldManager = fieldmanager.NewCRDFieldManager( + reqScope.Convertor, + reqScope.Defaulter, + reqScope.Kind.GroupVersion(), + reqScope.HubGroupVersion, + ) + requestScopes[v.Name] = reqScope } // override scaleSpec subresource values diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD index eb43054056..de11a45c45 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD @@ -9,6 +9,7 @@ load( go_test( name = "go_default_test", srcs = [ + "apply_test.go", "basic_test.go", "finalization_test.go", "objectmeta_test.go", @@ -41,6 +42,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library", "//staging/src/k8s.io/client-go/dynamic:go_default_library", diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/apply_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/apply_test.go new file mode 100644 index 0000000000..595a740075 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/apply_test.go @@ -0,0 +1,117 @@ +/* +Copyright 2018 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 integration + +import ( + "fmt" + "testing" + + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/test/integration/fixtures" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" + "k8s.io/client-go/dynamic" +) + +func TestApplyBasic(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + + tearDown, config, _, err := fixtures.StartDefaultServer(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + apiExtensionClient, err := clientset.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped) + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + + kind := noxuDefinition.Spec.Names.Kind + apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version + + rest := apiExtensionClient.Discovery().RESTClient() + yamlBody := []byte(fmt.Sprintf(` +apiVersion: %s +kind: %s +metadata: + name: mytest +values: + numVal: 1 + boolVal: true + stringVal: "1"`, apiVersion, kind)) + result, err := rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural). + Name("mytest"). + Body(yamlBody). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + + result, err = rest.Patch(types.MergePatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural). + Name("mytest"). + Body([]byte(`{"values":{"numVal": 5}}`)). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + + // Re-apply the same object, we should get conflicts now. + result, err = rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural). + Name("mytest"). + Body(yamlBody). + DoRaw() + if err == nil { + t.Fatalf("Expecting to get conflicts when applying object, got no error: %s", result) + } + status, ok := err.(*errors.StatusError) + if !ok { + t.Fatalf("Expecting to get conflicts as API error") + } + if len(status.Status().Details.Causes) < 1 { + t.Fatalf("Expecting to get at least one conflict when applying object, got: %v", status.Status().Details.Causes) + } + + // Re-apply with force, should work fine. + result, err = rest.Patch(types.ApplyPatchType). + AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural). + Name("mytest"). + Param("force", "true"). + Body(yamlBody). + DoRaw() + if err != nil { + t.Fatal(err, string(result)) + } + +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go index 3374caf2e4..b91b1e37fb 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer/fuzzer.go @@ -273,6 +273,12 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} { sort.Slice(j.MatchExpressions, func(a, b int) bool { return j.MatchExpressions[a].Key < j.MatchExpressions[b].Key }) } }, + func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) { + c.FuzzNoCustom(j) + if j.Fields != nil && len(j.Fields.Map) == 0 { + j.Fields = nil + } + }, } } diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/BUILD b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/BUILD index e05c8d4eb0..feb430e978 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/BUILD @@ -16,6 +16,7 @@ go_test( "helpers_test.go", "labels_test.go", "micro_time_test.go", + "options_test.go", "time_test.go", "types_test.go", ], diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go index ea12b929cd..7eecbd912d 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go @@ -63,8 +63,8 @@ type Object interface { SetOwnerReferences([]OwnerReference) GetClusterName() string SetClusterName(clusterName string) - GetManagedFields() map[string]VersionedFields - SetManagedFields(lastApplied map[string]VersionedFields) + GetManagedFields() []ManagedFieldsEntry + SetManagedFields(lastApplied []ManagedFieldsEntry) } // ListMetaAccessor retrieves the list interface from an object @@ -171,13 +171,13 @@ func (meta *ObjectMeta) SetOwnerReferences(references []OwnerReference) { func (meta *ObjectMeta) GetClusterName() string { return meta.ClusterName } func (meta *ObjectMeta) SetClusterName(clusterName string) { meta.ClusterName = clusterName } -func (meta *ObjectMeta) GetManagedFields() map[string]VersionedFields { +func (meta *ObjectMeta) GetManagedFields() []ManagedFieldsEntry { return meta.ManagedFields } -func (meta *ObjectMeta) SetManagedFields(ManagedFields map[string]VersionedFields) { - meta.ManagedFields = make(map[string]VersionedFields, len(ManagedFields)) - for key, value := range ManagedFields { - meta.ManagedFields[key] = value +func (meta *ObjectMeta) SetManagedFields(ManagedFields []ManagedFieldsEntry) { + meta.ManagedFields = make([]ManagedFieldsEntry, len(ManagedFields)) + for i, value := range ManagedFields { + meta.ManagedFields[i] = value } } diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go index 44f7773248..30c07984e0 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go @@ -253,15 +253,18 @@ type ObjectMeta struct { // +optional ClusterName string `json:"clusterName,omitempty" protobuf:"bytes,15,opt,name=clusterName"` - // ManagedFields is a map of workflow-id to the set of fields + // ManagedFields maps workflow-id and version to the set of fields // that are managed by that workflow. This is mostly for internal // housekeeping, and users typically shouldn't need to set or // understand this field. A workflow can be the user's name, a // controller's name, or the name of a specific apply path like // "ci-cd". The set of fields is always in the version that the // workflow used when modifying the object. + // + // This field is alpha and can be changed or removed without notice. + // // +optional - ManagedFields map[string]VersionedFields `json:"managedFields,omitempty" protobuf:"bytes,17,rep,name=managedFields"` + ManagedFields []ManagedFieldsEntry `json:"managedFields,omitempty" protobuf:"bytes,17,rep,name=managedFields"` } // Initializers tracks the progress of initialization. @@ -1043,18 +1046,35 @@ const ( LabelSelectorOpDoesNotExist LabelSelectorOperator = "DoesNotExist" ) -// VersionedFields is a pair of a FieldSet and the group version of the resource +// ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource // that the fieldset applies to. -type VersionedFields struct { +type ManagedFieldsEntry struct { + // Manager is an identifier of the workflow managing these fields. + Manager string `json:"manager,omitempty" protobuf:"bytes,1,opt,name=manager"` + // Operation is the type of operation which lead to this ManagedFieldsEntry being created. + // The only valid values for this field are 'Apply' and 'Update'. + Operation ManagedFieldsOperationType `json:"operation,omitempty" protobuf:"bytes,2,opt,name=operation,casttype=ManagedFieldsOperationType"` // APIVersion defines the version of this resource that this field set // applies to. The format is "group/version" just like the top-level // APIVersion field. It is necessary to track the version of a field // set because it cannot be automatically converted. - APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,1,opt,name=apiVersion"` + APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,3,opt,name=apiVersion"` + // Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply' + // +optional + Time *Time `json:"time,omitempty" protobuf:"bytes,4,opt,name=time"` // Fields identifies a set of fields. - Fields Fields `json:"fields,omitempty" protobuf:"bytes,2,opt,name=fields,casttype=Fields"` + // +optional + Fields *Fields `json:"fields,omitempty" protobuf:"bytes,5,opt,name=fields,casttype=Fields"` } +// ManagedFieldsOperationType is the type of operation which lead to a ManagedFieldsEntry being created. +type ManagedFieldsOperationType string + +const ( + ManagedFieldsOperationApply ManagedFieldsOperationType = "Apply" + ManagedFieldsOperationUpdate ManagedFieldsOperationType = "Update" +) + // Fields stores a set of fields in a data structure like a Trie. // To understand how this is used, see: https://github.com/kubernetes-sigs/structured-merge-diff type Fields struct { diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go index f24d65928a..1eaa85804f 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go @@ -143,13 +143,20 @@ func (u *Unstructured) setNestedField(value interface{}, fields ...string) { SetNestedField(u.Object, value, fields...) } -func (u *Unstructured) setNestedSlice(value []string, fields ...string) { +func (u *Unstructured) setNestedStringSlice(value []string, fields ...string) { if u.Object == nil { u.Object = make(map[string]interface{}) } SetNestedStringSlice(u.Object, value, fields...) } +func (u *Unstructured) setNestedSlice(value []interface{}, fields ...string) { + if u.Object == nil { + u.Object = make(map[string]interface{}) + } + SetNestedSlice(u.Object, value, fields...) +} + func (u *Unstructured) setNestedMap(value map[string]string, fields ...string) { if u.Object == nil { u.Object = make(map[string]interface{}) @@ -436,7 +443,7 @@ func (u *Unstructured) SetFinalizers(finalizers []string) { RemoveNestedField(u.Object, "metadata", "finalizers") return } - u.setNestedSlice(finalizers, "metadata", "finalizers") + u.setNestedStringSlice(finalizers, "metadata", "finalizers") } func (u *Unstructured) GetClusterName() string { @@ -451,27 +458,41 @@ func (u *Unstructured) SetClusterName(clusterName string) { u.setNestedField(clusterName, "metadata", "clusterName") } -func (u *Unstructured) GetManagedFields() map[string]metav1.VersionedFields { - m, found, err := nestedMapNoCopy(u.Object, "metadata", "managedFields") +func (u *Unstructured) GetManagedFields() []metav1.ManagedFieldsEntry { + items, found, err := NestedSlice(u.Object, "metadata", "managedFields") if !found || err != nil { return nil } - out := &map[string]metav1.VersionedFields{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, out); err != nil { - utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object: %v", err)) - return nil + managedFields := []metav1.ManagedFieldsEntry{} + for _, item := range items { + m, ok := item.(map[string]interface{}) + if !ok { + utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object, item %v is not a map", item)) + return nil + } + out := metav1.ManagedFieldsEntry{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, &out); err != nil { + utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object: %v", err)) + return nil + } + managedFields = append(managedFields, out) } - return *out + return managedFields } -func (u *Unstructured) SetManagedFields(managedFields map[string]metav1.VersionedFields) { +func (u *Unstructured) SetManagedFields(managedFields []metav1.ManagedFieldsEntry) { if managedFields == nil { RemoveNestedField(u.Object, "metadata", "managedFields") return } - out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&managedFields) - if err != nil { - utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object: %v", err)) + items := []interface{}{} + for _, managedFieldsEntry := range managedFields { + out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&managedFieldsEntry) + if err != nil { + utilruntime.HandleError(fmt.Errorf("unable to set managedFields for object: %v", err)) + return + } + items = append(items, out) } - u.setNestedField(out, "metadata", "managedFields") + u.setNestedSlice(items, "metadata", "managedFields") } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index b774f6c73c..cb7d20abf3 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -88,6 +88,7 @@ go_library( "//vendor/github.com/evanphx/json-patch:go_default_library", "//vendor/golang.org/x/net/websocket:go_default_library", "//vendor/k8s.io/klog:go_default_library", + "//vendor/k8s.io/utils/trace:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index 9a2f80e378..f8c2dd4c34 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "net/http" + "strings" "time" "k8s.io/apimachinery/pkg/api/errors" @@ -140,7 +141,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte return } - obj, err = scope.FieldManager.Update(liveObj, obj, "create") + obj, err = scope.FieldManager.Update(liveObj, obj, prefixFromUserAgent(req.UserAgent())) if err != nil { scope.err(fmt.Errorf("failed to update object managed fields: %v", err), w, req) return @@ -191,3 +192,7 @@ type namedCreaterAdapter struct { func (c *namedCreaterAdapter) Create(ctx context.Context, name string, obj runtime.Object, createValidatingAdmission rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { return c.Creater.Create(ctx, obj, createValidatingAdmission, options) } + +func prefixFromUserAgent(u string) string { + return strings.Split(u, "/")[0] +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/BUILD index 9ae6f2ce54..ce474dbbc4 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/BUILD @@ -8,6 +8,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go index 9e21d03acf..f4c1cdd3e2 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go @@ -18,8 +18,10 @@ package fieldmanager import ( "fmt" + "time" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" @@ -60,6 +62,22 @@ func NewFieldManager(models openapiproto.Models, objectConverter runtime.ObjectC }, nil } +// NewCRDFieldManager creates a new FieldManager specifically for +// CRDs. This doesn't use openapi models (and it doesn't support the +// validation field right now). +func NewCRDFieldManager(objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion) *FieldManager { + return &FieldManager{ + typeConverter: internal.DeducedTypeConverter{}, + objectConverter: objectConverter, + objectDefaulter: objectDefaulter, + groupVersion: gv, + hubVersion: hub, + updater: merge.Updater{ + Converter: internal.NewVersionConverter(internal.DeducedTypeConverter{}, objectConverter, hub), + }, + } +} + // Update is used when the object has already been merged (non-apply // use-case), and simply updates the managed fields in the output // object. @@ -97,6 +115,11 @@ func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (r return nil, fmt.Errorf("failed to create typed live object: %v", err) } apiVersion := fieldpath.APIVersion(f.groupVersion.String()) + manager, err = f.buildManagerInfo(manager, metav1.ManagedFieldsOperationUpdate) + if err != nil { + return nil, fmt.Errorf("failed to build manager identifier: %v", err) + } + managed, err = f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed, manager) if err != nil { return nil, fmt.Errorf("failed to update ManagedFields: %v", err) @@ -134,8 +157,13 @@ func (f *FieldManager) Apply(liveObj runtime.Object, patch []byte, force bool) ( if err != nil { return nil, fmt.Errorf("failed to create typed live object: %v", err) } + manager, err := f.buildManagerInfo(applyManager, metav1.ManagedFieldsOperationApply) + if err != nil { + return nil, fmt.Errorf("failed to build manager identifier: %v", err) + } + apiVersion := fieldpath.APIVersion(f.groupVersion.String()) - newObjTyped, managed, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed, applyManager, force) + newObjTyped, managed, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed, manager, force) if err != nil { if conflicts, ok := err.(merge.Conflicts); ok { return nil, errors.NewApplyConflict(conflicts) @@ -172,3 +200,16 @@ func (f *FieldManager) toVersioned(obj runtime.Object) (runtime.Object, error) { func (f *FieldManager) toUnversioned(obj runtime.Object) (runtime.Object, error) { return f.objectConverter.ConvertToVersion(obj, f.hubVersion) } + +func (f *FieldManager) buildManagerInfo(prefix string, operation metav1.ManagedFieldsOperationType) (string, error) { + managerInfo := metav1.ManagedFieldsEntry{ + Manager: prefix, + Operation: operation, + APIVersion: f.groupVersion.String(), + Time: &metav1.Time{Time: time.Now().UTC()}, + } + if managerInfo.Manager == "" { + managerInfo.Manager = "unknown" + } + return internal.DecodeManager(&managerInfo) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD index d8924d7289..8f0fe68bcb 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD @@ -38,7 +38,7 @@ go_test( "typeconverter_test.go", "versionconverter_test.go", ], - data = ["//api/openapi-spec:swagger-spec"], + data = ["//api/openapi-spec"], embed = [":go_default_library"], deps = [ "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/gvkparser.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/gvkparser.go index ce9a049244..14d90f964f 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/gvkparser.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/gvkparser.go @@ -35,7 +35,7 @@ type gvkParser struct { parser typed.Parser } -func (p *gvkParser) Type(gvk schema.GroupVersionKind) *typed.ParseableType { +func (p *gvkParser) Type(gvk schema.GroupVersionKind) typed.ParseableType { typeName, ok := p.gvks[gvk] if !ok { return nil diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go index ffe7485e98..b2f3264623 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go @@ -17,7 +17,9 @@ limitations under the License. package internal import ( + "encoding/json" "fmt" + "sort" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -72,21 +74,53 @@ func EncodeObjectManagedFields(obj runtime.Object, fields fieldpath.ManagedField // decodeManagedFields converts ManagedFields from the wire format (api format) // to the format used by sigs.k8s.io/structured-merge-diff -func decodeManagedFields(encodedManagedFields map[string]metav1.VersionedFields) (managedFields fieldpath.ManagedFields, err error) { +func decodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (managedFields fieldpath.ManagedFields, err error) { managedFields = make(map[string]*fieldpath.VersionedSet, len(encodedManagedFields)) - for manager, encodedVersionedSet := range encodedManagedFields { + for _, encodedVersionedSet := range encodedManagedFields { + manager, err := DecodeManager(&encodedVersionedSet) + if err != nil { + return nil, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err) + } managedFields[manager], err = decodeVersionedSet(&encodedVersionedSet) if err != nil { - return nil, fmt.Errorf("error decoding versioned set for %v: %v", manager, err) + return nil, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err) } } return managedFields, nil } -func decodeVersionedSet(encodedVersionedSet *metav1.VersionedFields) (versionedSet *fieldpath.VersionedSet, err error) { +// DecodeManager creates a manager identifier string from a ManagedFieldsEntry +func DecodeManager(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) { + encodedManagerCopy := *encodedManager + + // Never include the fields in the manager identifier + encodedManagerCopy.Fields = nil + + // For appliers, don't include the APIVersion or Time in the manager identifier, + // so it will always have the same manager identifier each time it applied. + if encodedManager.Operation == metav1.ManagedFieldsOperationApply { + encodedManagerCopy.APIVersion = "" + encodedManagerCopy.Time = nil + } + + // Use the remaining fields to build the manager identifier + b, err := json.Marshal(&encodedManagerCopy) + if err != nil { + return "", fmt.Errorf("error marshalling manager identifier: %v", err) + } + + return string(b), nil +} + +func decodeVersionedSet(encodedVersionedSet *metav1.ManagedFieldsEntry) (versionedSet *fieldpath.VersionedSet, err error) { versionedSet = &fieldpath.VersionedSet{} versionedSet.APIVersion = fieldpath.APIVersion(encodedVersionedSet.APIVersion) - set, err := FieldsToSet(encodedVersionedSet.Fields) + + fields := metav1.Fields{} + if encodedVersionedSet.Fields != nil { + fields = *encodedVersionedSet.Fields + } + set, err := FieldsToSet(fields) if err != nil { return nil, fmt.Errorf("error decoding set: %v", err) } @@ -96,24 +130,42 @@ func decodeVersionedSet(encodedVersionedSet *metav1.VersionedFields) (versionedS // encodeManagedFields converts ManagedFields from the the format used by // sigs.k8s.io/structured-merge-diff to the the wire format (api format) -func encodeManagedFields(managedFields fieldpath.ManagedFields) (encodedManagedFields map[string]metav1.VersionedFields, err error) { - encodedManagedFields = make(map[string]metav1.VersionedFields, len(managedFields)) - for manager, versionedSet := range managedFields { - v, err := encodeVersionedSet(versionedSet) +func encodeManagedFields(managedFields fieldpath.ManagedFields) (encodedManagedFields []metav1.ManagedFieldsEntry, err error) { + // Sort the keys so a predictable order will be used. + managers := []string{} + for manager := range managedFields { + managers = append(managers, manager) + } + sort.Strings(managers) + + encodedManagedFields = []metav1.ManagedFieldsEntry{} + for _, manager := range managers { + versionedSet := managedFields[manager] + v, err := encodeManagerVersionedSet(manager, versionedSet) if err != nil { return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err) } - encodedManagedFields[manager] = *v + encodedManagedFields = append(encodedManagedFields, *v) } return encodedManagedFields, nil } -func encodeVersionedSet(versionedSet *fieldpath.VersionedSet) (encodedVersionedSet *metav1.VersionedFields, err error) { - encodedVersionedSet = &metav1.VersionedFields{} +func encodeManagerVersionedSet(manager string, versionedSet *fieldpath.VersionedSet) (encodedVersionedSet *metav1.ManagedFieldsEntry, err error) { + encodedVersionedSet = &metav1.ManagedFieldsEntry{} + + // Get as many fields as we can from the manager identifier + err = json.Unmarshal([]byte(manager), encodedVersionedSet) + if err != nil { + return nil, fmt.Errorf("error unmarshalling manager identifier %v: %v", manager, err) + } + + // Get the APIVersion and Fields from the VersionedSet encodedVersionedSet.APIVersion = string(versionedSet.APIVersion) - encodedVersionedSet.Fields, err = SetToFields(*versionedSet.Set) + fields, err := SetToFields(*versionedSet.Set) if err != nil { return nil, fmt.Errorf("error encoding set: %v", err) } + encodedVersionedSet.Fields = &fields + return encodedVersionedSet, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go index dca8b93c73..7f2d517f57 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go @@ -29,29 +29,36 @@ import ( // sigs.k8s.io/structured-merge-diff to the wire format (api format) and back func TestRoundTripManagedFields(t *testing.T) { tests := []string{ - `foo: - apiVersion: v1 + `- apiVersion: v1 fields: - i:5: - f:i: {} v:3: f:alsoPi: {} v:3.1415: f:pi: {} v:false: f:notTrue: {} + manager: foo + operation: Update + time: "2001-02-03T04:05:06Z" +- apiVersion: v1beta1 + fields: + i:5: + f:i: {} + manager: foo + operation: Update + time: "2011-12-13T14:15:16Z" `, - `foo: - apiVersion: v1 + `- apiVersion: v1 fields: f:spec: f:containers: k:{"name":"c"}: f:image: {} f:name: {} + manager: foo + operation: Apply `, - `foo: - apiVersion: v1 + `- apiVersion: v1 fields: f:apiVersion: {} f:kind: {} @@ -77,9 +84,10 @@ func TestRoundTripManagedFields(t *testing.T) { f:ports: i:0: f:containerPort: {} + manager: foo + operation: Update `, - `foo: - apiVersion: v1 + `- apiVersion: v1 fields: f:allowVolumeExpansion: {} f:apiVersion: {} @@ -92,9 +100,10 @@ func TestRoundTripManagedFields(t *testing.T) { f:secretName: {} f:secretNamespace: {} f:provisioner: {} + manager: foo + operation: Apply `, - `foo: - apiVersion: v1 + `- apiVersion: v1 fields: f:apiVersion: {} f:kind: {} @@ -114,12 +123,14 @@ func TestRoundTripManagedFields(t *testing.T) { f:name: {} f:served: {} f:storage: {} + manager: foo + operation: Update `, } for _, test := range tests { t.Run(test, func(t *testing.T) { - var unmarshaled map[string]metav1.VersionedFields + var unmarshaled []metav1.ManagedFieldsEntry if err := yaml.Unmarshal([]byte(test), &unmarshaled); err != nil { t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) } @@ -141,3 +152,49 @@ func TestRoundTripManagedFields(t *testing.T) { }) } } + +func TestDecodeManager(t *testing.T) { + tests := []struct { + managedFieldsEntry string + expected string + }{ + { + managedFieldsEntry: ` +apiVersion: v1 +fields: + f:apiVersion: {} +manager: foo +operation: Update +time: "2001-02-03T04:05:06Z" +`, + expected: "{\"manager\":\"foo\",\"operation\":\"Update\",\"apiVersion\":\"v1\",\"time\":\"2001-02-03T04:05:06Z\"}", + }, + { + managedFieldsEntry: ` +apiVersion: v1 +fields: + f:apiVersion: {} +manager: foo +operation: Apply +time: "2001-02-03T04:05:06Z" +`, + expected: "{\"manager\":\"foo\",\"operation\":\"Apply\"}", + }, + } + + for _, test := range tests { + t.Run(test.managedFieldsEntry, func(t *testing.T) { + var unmarshaled metav1.ManagedFieldsEntry + if err := yaml.Unmarshal([]byte(test.managedFieldsEntry), &unmarshaled); err != nil { + t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) + } + decoded, err := DecodeManager(&unmarshaled) + if err != nil { + t.Fatalf("did not expect decoding error but got: %v", err) + } + if !reflect.DeepEqual(decoded, test.expected) { + t.Fatalf("expected:\n%v\nbut got:\n%v", test.expected, decoded) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go index 79f3017cb4..b4974752de 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go @@ -21,21 +21,52 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/kube-openapi/pkg/util/proto" "sigs.k8s.io/structured-merge-diff/typed" + "sigs.k8s.io/structured-merge-diff/value" "sigs.k8s.io/yaml" ) // TypeConverter allows you to convert from runtime.Object to // typed.TypedValue and the other way around. type TypeConverter interface { - NewTyped(schema.GroupVersionKind) (typed.TypedValue, error) ObjectToTyped(runtime.Object) (typed.TypedValue, error) YAMLToTyped([]byte) (typed.TypedValue, error) TypedToObject(typed.TypedValue) (runtime.Object, error) } +// DeducedTypeConverter is a TypeConverter for CRDs that don't have a +// schema. It does implement the same interface though (and create the +// same types of objects), so that everything can still work the same. +// CRDs are merged with all their fields being "atomic" (lists +// included). +// +// Note that this is not going to be sufficient for converting to/from +// CRDs that have a schema defined (we don't support that schema yet). +type DeducedTypeConverter struct{} + +var _ TypeConverter = DeducedTypeConverter{} + +// ObjectToTyped converts an object into a TypedValue with a "deduced type". +func (DeducedTypeConverter) ObjectToTyped(obj runtime.Object) (typed.TypedValue, error) { + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + return typed.DeducedParseableType{}.FromUnstructured(u) +} + +// YAMLToTyped parses a yaml object into a TypedValue with a "deduced type". +func (DeducedTypeConverter) YAMLToTyped(from []byte) (typed.TypedValue, error) { + return typed.DeducedParseableType{}.FromYAML(typed.YAMLObject(from)) +} + +// TypedToObject transforms the typed value into a runtime.Object. That +// is not specific to deduced type. +func (DeducedTypeConverter) TypedToObject(value typed.TypedValue) (runtime.Object, error) { + return valueToObject(value.AsValue()) +} + type typeConverter struct { parser *gvkParser } @@ -53,28 +84,15 @@ func NewTypeConverter(models proto.Models) (TypeConverter, error) { return &typeConverter{parser: parser}, nil } -func (c *typeConverter) NewTyped(gvk schema.GroupVersionKind) (typed.TypedValue, error) { - t := c.parser.Type(gvk) - if t == nil { - return typed.TypedValue{}, fmt.Errorf("no corresponding type for %v", gvk) - } - - u, err := t.New() - if err != nil { - return typed.TypedValue{}, fmt.Errorf("new typed: %v", err) - } - return u, nil -} - func (c *typeConverter) ObjectToTyped(obj runtime.Object) (typed.TypedValue, error) { u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { - return typed.TypedValue{}, err + return nil, err } gvk := obj.GetObjectKind().GroupVersionKind() t := c.parser.Type(gvk) if t == nil { - return typed.TypedValue{}, fmt.Errorf("no corresponding type for %v", gvk) + return nil, fmt.Errorf("no corresponding type for %v", gvk) } return t.FromUnstructured(u) } @@ -83,14 +101,18 @@ func (c *typeConverter) YAMLToTyped(from []byte) (typed.TypedValue, error) { unstructured := &unstructured.Unstructured{Object: map[string]interface{}{}} if err := yaml.Unmarshal(from, &unstructured.Object); err != nil { - return typed.TypedValue{}, fmt.Errorf("error decoding YAML: %v", err) + return nil, fmt.Errorf("error decoding YAML: %v", err) } return c.ObjectToTyped(unstructured) } func (c *typeConverter) TypedToObject(value typed.TypedValue) (runtime.Object, error) { - vu := value.AsValue().ToUnstructured(false) + return valueToObject(value.AsValue()) +} + +func valueToObject(value *value.Value) (runtime.Object, error) { + vu := value.ToUnstructured(false) u, ok := vu.(map[string]interface{}) if !ok { return nil, fmt.Errorf("failed to convert typed to unstructured: want map, got %T", vu) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index b674dcdd4b..2d0d5f0807 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -201,6 +201,7 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface name: name, patchType: patchType, patchBytes: patchBytes, + userAgent: req.UserAgent(), trace: trace, } @@ -268,6 +269,7 @@ type patcher struct { name string patchType types.PatchType patchBytes []byte + userAgent string trace *utiltrace.Trace @@ -309,7 +311,7 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r } if p.fieldManager != nil { - if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, "jsonPatcher"); err != nil { + if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, prefixFromUserAgent(p.userAgent)); err != nil { return nil, fmt.Errorf("failed to update object managed fields: %v", err) } } @@ -371,7 +373,7 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru } if p.fieldManager != nil { - if newObj, err = p.fieldManager.Update(currentObject, newObj, "smPatcher"); err != nil { + if newObj, err = p.fieldManager.Update(currentObject, newObj, prefixFromUserAgent(p.userAgent)); err != nil { return nil, fmt.Errorf("failed to update object managed fields: %v", err) } } @@ -395,6 +397,9 @@ func (p *applyPatcher) applyPatchToCurrentObject(obj runtime.Object) (runtime.Ob if p.options.Force != nil { force = *p.options.Force } + if p.fieldManager == nil { + panic("FieldManager must be installed to run apply") + } return p.fieldManager.Apply(obj, p.patch, force) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index e78346c5b0..a57e913d5b 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -123,8 +123,8 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac userInfo, _ := request.UserFrom(ctx) transformers := []rest.TransformFunc{} if scope.FieldManager != nil { - transformers = append(transformers, func(_ context.Context, liveObj, newObj runtime.Object) (runtime.Object, error) { - if obj, err = scope.FieldManager.Update(liveObj, newObj, "update"); err != nil { + transformers = append(transformers, func(_ context.Context, newObj, liveObj runtime.Object) (runtime.Object, error) { + if obj, err = scope.FieldManager.Update(liveObj, newObj, prefixFromUserAgent(req.UserAgent())); err != nil { return nil, fmt.Errorf("failed to update object managed fields: %v", err) } return obj, nil diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go index a630fd4e05..f96c80c8bd 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go @@ -191,7 +191,7 @@ func TestPatchApply(t *testing.T) { if simpleStorage.updated.Other != "bar" { t.Errorf(`Merge should have kept initial "bar" value for Other: %v`, simpleStorage.updated.Other) } - if _, ok := simpleStorage.updated.ObjectMeta.ManagedFields["default"]; !ok { + if len(simpleStorage.updated.ObjectMeta.ManagedFields) == 0 { t.Errorf(`Expected managedFields field to be set, but is empty`) } } @@ -230,11 +230,11 @@ func TestApplyAddsGVK(t *testing.T) { } // TODO: Need to fix this expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}` - if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected { + if simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion != expected { t.Errorf( `Expected managedFields field to be %q, got %q`, expected, - simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion, + simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion, ) } } @@ -265,11 +265,11 @@ func TestApplyCreatesWithManagedFields(t *testing.T) { } // TODO: Need to fix this expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}` - if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected { + if simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion != expected { t.Errorf( `Expected managedFields field to be %q, got %q`, expected, - simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion, + simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion, ) } } diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD b/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD index 37d58875c5..7ad6f3b6fa 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD @@ -48,7 +48,9 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/test/integration/apiserver/apply/BUILD b/test/integration/apiserver/apply/BUILD index d5633a713d..a8bb1c1f59 100644 --- a/test/integration/apiserver/apply/BUILD +++ b/test/integration/apiserver/apply/BUILD @@ -9,6 +9,7 @@ go_test( deps = [ "//pkg/master:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", diff --git a/test/integration/apiserver/apply/apply_test.go b/test/integration/apiserver/apply/apply_test.go index 4bbbaf5980..f66f6a171a 100644 --- a/test/integration/apiserver/apply/apply_test.go +++ b/test/integration/apiserver/apply/apply_test.go @@ -17,10 +17,13 @@ limitations under the License. package apiserver import ( + "encoding/json" "net/http/httptest" "testing" + "time" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" genericfeatures "k8s.io/apiserver/pkg/features" @@ -204,3 +207,98 @@ func TestApplyUpdateApplyConflictForced(t *testing.T) { t.Fatalf("Failed to apply object with force: %v", err) } } + +// TestApplyManagedFields makes sure that managedFields api does not change +func TestApplyManagedFields(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + Namespace("default"). + Resource("configmaps"). + Name("test-cm"). + Body([]byte(`{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "test-cm" + }, + "data": { + "key": "value" + } + }`)). + Do(). + Get() + if err != nil { + t.Fatalf("Failed to create object using Apply patch: %v", err) + } + + _, err = client.CoreV1().RESTClient().Patch(types.MergePatchType). + Namespace("default"). + Resource("configmaps"). + Name("test-cm"). + Body([]byte(`{"data":{"key": "new value"}}`)).Do().Get() + if err != nil { + t.Fatalf("Failed to patch object: %v", err) + } + + object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do().Get() + if err != nil { + t.Fatalf("Failed to retrieve object: %v", err) + } + + accessor, err := meta.Accessor(object) + if err != nil { + t.Fatalf("Failed to get meta accessor: %v", err) + } + + actual, err := json.MarshalIndent(object, "\t", "\t") + if err != nil { + t.Fatalf("Failed to marshal object: %v", err) + } + + expected := []byte(`{ + "metadata": { + "name": "test-cm", + "namespace": "default", + "selfLink": "` + accessor.GetSelfLink() + `", + "uid": "` + string(accessor.GetUID()) + `", + "resourceVersion": "` + accessor.GetResourceVersion() + `", + "creationTimestamp": "` + accessor.GetCreationTimestamp().UTC().Format(time.RFC3339) + `", + "managedFields": [ + { + "manager": "apply", + "operation": "Apply", + "apiVersion": "v1", + "fields": { + "f:apiVersion": {}, + "f:kind": {}, + "f:metadata": { + "f:name": {} + } + } + }, + { + "manager": "` + accessor.GetManagedFields()[1].Manager + `", + "operation": "Update", + "apiVersion": "v1", + "time": "` + accessor.GetManagedFields()[1].Time.UTC().Format(time.RFC3339) + `", + "fields": { + "f:data": { + "f:key": {} + } + } + } + ] + }, + "data": { + "key": "new value" + } + }`) + + if string(expected) != string(actual) { + t.Fatalf("Expected:\n%v\nGot:\n%v", string(expected), string(actual)) + } +}