Plugin FieldManager in CRD handler, change to API

pull/564/head
Antoine Pelisse 2019-01-29 14:24:52 -08:00
parent 1751fc013f
commit b55417f429
27 changed files with 595 additions and 117 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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))
}
}

View File

@ -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
}
},
}
}

View File

@ -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",
],

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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")
}

View File

@ -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",
],
)

View File

@ -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]
}

View File

@ -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",

View File

@ -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)
}

View File

@ -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",

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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,
)
}
}

View File

@ -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",
],
)

View File

@ -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",

View File

@ -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))
}
}