From adfc65ec23292ebd95d96727654acd8e8e0452a5 Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Mon, 12 Dec 2022 14:07:13 -0800 Subject: [PATCH 1/2] Make internal managers private This significantly reduces the surface area of the fieldmanager package by hiding all the private "managers" objects, as well as the interface that was made specifically for these. There is no reason to configure these. --- .../pkg/apiserver/customresource_handler.go | 2 +- .../handlers/fieldmanager/admission.go | 3 +- .../handlers/fieldmanager/fieldmanager.go | 225 +----------------- .../fieldmanagertest/testfieldmanager.go | 27 ++- .../{ => internal}/buildmanagerinfo.go | 5 +- .../{ => internal}/capmanagers.go | 5 +- .../{ => internal}/capmanagers_test.go | 18 +- .../fieldmanager/internal/fieldmanager.go | 206 ++++++++++++++++ .../internal/fieldmanager_test.go | 61 +++++ .../{ => internal}/lastappliedmanager.go | 2 +- .../{ => internal}/lastappliedmanager_test.go | 28 ++- .../{ => internal}/lastappliedupdater.go | 5 +- .../{ => internal}/lastappliedupdater_test.go | 30 ++- .../{ => internal}/managedfieldsupdater.go | 5 +- .../managedfieldsupdater_test.go | 11 +- .../handlers/fieldmanager/internal/manager.go | 52 ++++ .../{ => internal}/skipnonapplied.go | 2 +- .../{ => internal}/skipnonapplied_test.go | 12 +- .../fieldmanager/{ => internal}/stripmeta.go | 2 +- .../{ => internal}/structuredmerge.go | 7 +- .../{ => internal}/testdata/swagger.json | 0 .../fieldmanager/internal/typeconverter.go | 116 +++++++++ .../{ => internal}/typeconverter_test.go | 28 +-- .../{ => internal}/versionconverter.go | 24 +- .../{ => internal}/versionconverter_test.go | 20 +- .../handlers/fieldmanager/scalehandler.go | 6 +- .../handlers/fieldmanager/typeconverter.go | 109 +-------- .../invalid_managedFields_test.go | 3 +- .../apiserver/timestamp_transformer_test.go | 2 +- 29 files changed, 601 insertions(+), 415 deletions(-) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/buildmanagerinfo.go (94%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/capmanagers.go (96%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/capmanagers_test.go (94%) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager.go create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/lastappliedmanager.go (99%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/lastappliedmanager_test.go (95%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/lastappliedupdater.go (95%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/lastappliedupdater_test.go (88%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/managedfieldsupdater.go (95%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/managedfieldsupdater_test.go (96%) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/manager.go rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/skipnonapplied.go (99%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/skipnonapplied_test.go (91%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/stripmeta.go (99%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/structuredmerge.go (97%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/testdata/swagger.json (100%) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/typeconverter_test.go (83%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/versionconverter.go (87%) rename staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/{ => internal}/versionconverter_test.go (87%) 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 298b3afa7e1..c6447e62cbd 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 @@ -679,7 +679,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd openAPIModels = nil } - var typeConverter fieldmanager.TypeConverter = fieldmanager.DeducedTypeConverter{} + var typeConverter fieldmanager.TypeConverter = fieldmanager.NewDeducedTypeConverter() if openAPIModels != nil { typeConverter, err = fieldmanager.NewTypeConverter(openAPIModels, crd.Spec.PreserveUnknownFields) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go index 26d264fe833..5521f312851 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "k8s.io/apiserver/pkg/warning" ) @@ -70,7 +71,7 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte return err } managedFieldsAfterAdmission := objectMeta.GetManagedFields() - if _, err := DecodeManagedFields(managedFieldsAfterAdmission); err != nil { + if _, err := internal.DecodeManagedFields(managedFieldsAfterAdmission); err != nil { objectMeta.SetManagedFields(managedFieldsBeforeAdmission) warning.AddWarning(ctx, "", fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, 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 6c3d2ce8326..668f7688261 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,247 +18,40 @@ package fieldmanager import ( "fmt" - "reflect" - "time" - "k8s.io/apimachinery/pkg/api/meta" 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" - "k8s.io/klog/v2" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" - "sigs.k8s.io/structured-merge-diff/v4/merge" ) -// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates -// if the number of update managers exceeds this, the oldest entries will be merged until the number is below the maximum. -// TODO(jennybuckley): Determine if this is really the best value. Ideally we wouldn't unnecessarily merge too many entries. -const DefaultMaxUpdateManagers int = 10 - -// DefaultTrackOnCreateProbability defines the default probability that the field management of an object -// starts being tracked from the object's creation, instead of from the first time the object is applied to. -const DefaultTrackOnCreateProbability float32 = 1 - -var atMostEverySecond = internal.NewAtMostEvery(time.Second) - -// Managed groups a fieldpath.ManagedFields together with the timestamps associated with each operation. -type Managed interface { - // Fields gets the fieldpath.ManagedFields. - Fields() fieldpath.ManagedFields - - // Times gets the timestamps associated with each operation. - Times() map[string]*metav1.Time -} - -// Manager updates the managed fields and merges applied configurations. -type Manager interface { - // Update is used when the object has already been merged (non-apply - // use-case), and simply updates the managed fields in the output - // object. - // * `liveObj` is not mutated by this function - // * `newObj` may be mutated by this function - // Returns the new object with managedFields removed, and the object's new - // proposed managedFields separately. - Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) - - // Apply is used when server-side apply is called, as it merges the - // object and updates the managed fields. - // * `liveObj` is not mutated by this function - // * `newObj` may be mutated by this function - // Returns the new object with managedFields removed, and the object's new - // proposed managedFields separately. - Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) -} - -// FieldManager updates the managed fields and merge applied +// FieldManager updates the managed fields and merges applied // configurations. -type FieldManager struct { - fieldManager Manager - subresource string -} - -// NewFieldManager creates a new FieldManager that decodes, manages, then re-encodes managedFields -// on update and apply requests. -func NewFieldManager(f Manager, subresource string) *FieldManager { - return &FieldManager{fieldManager: f, subresource: subresource} -} +type FieldManager = internal.FieldManager // NewDefaultFieldManager creates a new FieldManager that merges apply requests // and update managed fields for other types of requests. func NewDefaultFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (*FieldManager, error) { - f, err := NewStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields) + f, err := internal.NewStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields) if err != nil { return nil, fmt.Errorf("failed to create field manager: %v", err) } - return newDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil + return internal.NewDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil } // NewDefaultCRDFieldManager creates a new FieldManager specifically for // CRDs. This allows for the possibility of fields which are not defined // in models, as well as having no models defined at all. func NewDefaultCRDFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (_ *FieldManager, err error) { - f, err := NewCRDStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields) + f, err := internal.NewCRDStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields) if err != nil { return nil, fmt.Errorf("failed to create field manager: %v", err) } - return newDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil + return internal.NewDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil } -// newDefaultFieldManager is a helper function which wraps a Manager with certain default logic. -func newDefaultFieldManager(f Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, subresource string) *FieldManager { - return NewFieldManager( - NewLastAppliedUpdater( - NewLastAppliedManager( - NewProbabilisticSkipNonAppliedManager( - NewCapManagersManager( - NewBuildManagerInfoManager( - NewManagedFieldsUpdater( - NewStripMetaManager(f), - ), kind.GroupVersion(), subresource, - ), DefaultMaxUpdateManagers, - ), objectCreater, kind, DefaultTrackOnCreateProbability, - ), typeConverter, objectConverter, kind.GroupVersion()), - ), subresource, - ) -} - -// DecodeManagedFields converts ManagedFields from the wire format (api format) -// to the format used by sigs.k8s.io/structured-merge-diff -func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (Managed, error) { - return internal.DecodeManagedFields(encodedManagedFields) -} - -func decodeLiveOrNew(liveObj, newObj runtime.Object, ignoreManagedFieldsFromRequestObject bool) (Managed, error) { - liveAccessor, err := meta.Accessor(liveObj) - if err != nil { - return nil, err - } - - // We take the managedFields of the live object in case the request tries to - // manually set managedFields via a subresource. - if ignoreManagedFieldsFromRequestObject { - return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields())) - } - - // If the object doesn't have metadata, we should just return without trying to - // set the managedFields at all, so creates/updates/patches will work normally. - newAccessor, err := meta.Accessor(newObj) - if err != nil { - return nil, err - } - - if isResetManagedFields(newAccessor.GetManagedFields()) { - return internal.NewEmptyManaged(), nil - } - - // If the managed field is empty or we failed to decode it, - // let's try the live object. This is to prevent clients who - // don't understand managedFields from deleting it accidentally. - managed, err := DecodeManagedFields(newAccessor.GetManagedFields()) - if err != nil || len(managed.Fields()) == 0 { - return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields())) - } - return managed, nil -} - -func emptyManagedFieldsOnErr(managed Managed, err error) (Managed, error) { - if err != nil { - return internal.NewEmptyManaged(), nil - } - return managed, nil -} - -// Update is used when the object has already been merged (non-apply -// use-case), and simply updates the managed fields in the output -// object. -func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (object runtime.Object, err error) { - // First try to decode the managed fields provided in the update, - // This is necessary to allow directly updating managed fields. - isSubresource := f.subresource != "" - managed, err := decodeLiveOrNew(liveObj, newObj, isSubresource) - if err != nil { - return newObj, nil - } - - internal.RemoveObjectManagedFields(newObj) - - if object, managed, err = f.fieldManager.Update(liveObj, newObj, managed, manager); err != nil { - return nil, err - } - - if err = internal.EncodeObjectManagedFields(object, managed); err != nil { - return nil, fmt.Errorf("failed to encode managed fields: %v", err) - } - - return object, nil -} - -// UpdateNoErrors is the same as Update, but it will not return -// errors. If an error happens, the object is returned with -// managedFields cleared. -func (f *FieldManager) UpdateNoErrors(liveObj, newObj runtime.Object, manager string) runtime.Object { - obj, err := f.Update(liveObj, newObj, manager) - if err != nil { - atMostEverySecond.Do(func() { - ns, name := "unknown", "unknown" - if accessor, err := meta.Accessor(newObj); err == nil { - ns = accessor.GetNamespace() - name = accessor.GetName() - } - - klog.ErrorS(err, "[SHOULD NOT HAPPEN] failed to update managedFields", "VersionKind", - newObj.GetObjectKind().GroupVersionKind(), "namespace", ns, "name", name) - }) - // Explicitly remove managedFields on failure, so that - // we can't have garbage in it. - internal.RemoveObjectManagedFields(newObj) - return newObj - } - return obj -} - -// Returns true if the managedFields indicate that the user is trying to -// reset the managedFields, i.e. if the list is non-nil but empty, or if -// the list has one empty item. -func isResetManagedFields(managedFields []metav1.ManagedFieldsEntry) bool { - if len(managedFields) == 0 { - return managedFields != nil - } - - if len(managedFields) == 1 { - return reflect.DeepEqual(managedFields[0], metav1.ManagedFieldsEntry{}) - } - - return false -} - -// Apply is used when server-side apply is called, as it merges the -// object and updates the managed fields. -func (f *FieldManager) Apply(liveObj, appliedObj runtime.Object, manager string, force bool) (object runtime.Object, err error) { - // If the object doesn't have metadata, apply isn't allowed. - accessor, err := meta.Accessor(liveObj) - if err != nil { - return nil, fmt.Errorf("couldn't get accessor: %v", err) - } - - // Decode the managed fields in the live object, since it isn't allowed in the patch. - managed, err := DecodeManagedFields(accessor.GetManagedFields()) - if err != nil { - return nil, fmt.Errorf("failed to decode managed fields: %v", err) - } - - object, managed, err = f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force) - if err != nil { - if conflicts, ok := err.(merge.Conflicts); ok { - return nil, internal.NewConflictError(conflicts) - } - return nil, err - } - - if err = internal.EncodeObjectManagedFields(object, managed); err != nil { - return nil, fmt.Errorf("failed to encode managed fields: %v", err) - } - - return object, nil +func ValidateManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) error { + _, err := internal.DecodeManagedFields(encodedManagedFields) + return err } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest/testfieldmanager.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest/testfieldmanager.go index 53d0154f01d..71eb9e6ee32 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest/testfieldmanager.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest/testfieldmanager.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" "sigs.k8s.io/structured-merge-diff/v4/merge" "sigs.k8s.io/structured-merge-diff/v4/typed" @@ -95,7 +96,7 @@ func (f *fakeObjectCreater) New(gvk schema.GroupVersionKind) (runtime.Object, er // to specify either a sub-resource, or a set of modified Manager to // test them specifically. type TestFieldManager struct { - fieldManager *fieldmanager.FieldManager + fieldManager *internal.FieldManager apiVersion string emptyObj runtime.Object liveObj runtime.Object @@ -108,10 +109,10 @@ func NewDefaultTestFieldManager(typeConverter fieldmanager.TypeConverter, gvk sc } // NewTestFieldManager creates a new manager for the given GVK. -func NewTestFieldManager(typeConverter fieldmanager.TypeConverter, gvk schema.GroupVersionKind, subresource string, chainFieldManager func(fieldmanager.Manager) fieldmanager.Manager) TestFieldManager { +func NewTestFieldManager(typeConverter fieldmanager.TypeConverter, gvk schema.GroupVersionKind, subresource string, chainFieldManager func(internal.Manager) internal.Manager) TestFieldManager { apiVersion := fieldpath.APIVersion(gvk.GroupVersion().String()) objectConverter := &fakeObjectConvertor{sameVersionConverter{}, apiVersion} - f, err := fieldmanager.NewStructuredMergeManager( + f, err := internal.NewStructuredMergeManager( typeConverter, objectConverter, &fakeObjectDefaulter{}, @@ -125,14 +126,18 @@ func NewTestFieldManager(typeConverter fieldmanager.TypeConverter, gvk schema.Gr live := &unstructured.Unstructured{} live.SetKind(gvk.Kind) live.SetAPIVersion(gvk.GroupVersion().String()) - f = fieldmanager.NewLastAppliedUpdater( - fieldmanager.NewLastAppliedManager( - fieldmanager.NewProbabilisticSkipNonAppliedManager( - fieldmanager.NewBuildManagerInfoManager( - fieldmanager.NewManagedFieldsUpdater( - fieldmanager.NewStripMetaManager(f), + // This is different from `internal.NewDefaultFieldManager` because: + // 1. We don't want to create a `internal.FieldManager` + // 2. We don't want to use the CapManager that is tested separately with + // a smaller than the default cap. + f = internal.NewLastAppliedUpdater( + internal.NewLastAppliedManager( + internal.NewProbabilisticSkipNonAppliedManager( + internal.NewBuildManagerInfoManager( + internal.NewManagedFieldsUpdater( + internal.NewStripMetaManager(f), ), gvk.GroupVersion(), subresource, - ), NewFakeObjectCreater(), gvk, fieldmanager.DefaultTrackOnCreateProbability, + ), NewFakeObjectCreater(), gvk, internal.DefaultTrackOnCreateProbability, ), typeConverter, objectConverter, gvk.GroupVersion(), ), ) @@ -140,7 +145,7 @@ func NewTestFieldManager(typeConverter fieldmanager.TypeConverter, gvk schema.Gr f = chainFieldManager(f) } return TestFieldManager{ - fieldManager: fieldmanager.NewFieldManager(f, subresource), + fieldManager: internal.NewFieldManager(f, subresource), apiVersion: gvk.GroupVersion().String(), emptyObj: live, liveObj: live.DeepCopyObject(), diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/buildmanagerinfo.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/buildmanagerinfo.go similarity index 94% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/buildmanagerinfo.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/buildmanagerinfo.go index 58b87eb3886..fa342ca1351 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/buildmanagerinfo.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/buildmanagerinfo.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "fmt" @@ -22,7 +22,6 @@ import ( 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" ) type buildManagerInfoManager struct { @@ -71,5 +70,5 @@ func (f *buildManagerInfoManager) buildManagerInfo(prefix string, operation meta if managerInfo.Manager == "" { managerInfo.Manager = "unknown" } - return internal.BuildManagerIdentifier(&managerInfo) + return BuildManagerIdentifier(&managerInfo) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/capmanagers.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/capmanagers.go similarity index 96% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/capmanagers.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/capmanagers.go index c3184e24154..8951932ba42 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/capmanagers.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/capmanagers.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "fmt" @@ -22,7 +22,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) @@ -100,7 +99,7 @@ func (f *capManagersManager) capUpdateManagers(managed Managed) (newManaged Mana // Create a new manager identifier for the versioned bucket entry. // The version for this manager comes from the version of the update being merged into the bucket. - bucket, err := internal.BuildManagerIdentifier(&metav1.ManagedFieldsEntry{ + bucket, err := BuildManagerIdentifier(&metav1.ManagedFieldsEntry{ Manager: f.oldUpdatesManagerName, Operation: metav1.ManagedFieldsOperationUpdate, APIVersion: version, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/capmanagers_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/capmanagers_test.go similarity index 94% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/capmanagers_test.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/capmanagers_test.go index b0ee5a0259d..b98eb37dab9 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/capmanagers_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/capmanagers_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager_test +package internal_test import ( "bytes" @@ -29,20 +29,20 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) type fakeManager struct{} -var _ fieldmanager.Manager = &fakeManager{} +var _ internal.Manager = &fakeManager{} -func (*fakeManager) Update(_, newObj runtime.Object, managed fieldmanager.Managed, _ string) (runtime.Object, fieldmanager.Managed, error) { +func (*fakeManager) Update(_, newObj runtime.Object, managed internal.Managed, _ string) (runtime.Object, internal.Managed, error) { return newObj, managed, nil } -func (*fakeManager) Apply(_, _ runtime.Object, _ fieldmanager.Managed, _ string, _ bool) (runtime.Object, fieldmanager.Managed, error) { +func (*fakeManager) Apply(_, _ runtime.Object, _ internal.Managed, _ string, _ bool) (runtime.Object, internal.Managed, error) { panic("not implemented") return nil, nil, nil } @@ -50,8 +50,8 @@ func (*fakeManager) Apply(_, _ runtime.Object, _ fieldmanager.Managed, _ string, func TestCapManagersManagerMergesEntries(t *testing.T) { f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", - func(m fieldmanager.Manager) fieldmanager.Manager { - return fieldmanager.NewCapManagersManager(m, 3) + func(m internal.Manager) internal.Manager { + return internal.NewCapManagersManager(m, 3) }) podWithLabels := func(labels ...string) runtime.Object { @@ -116,8 +116,8 @@ func TestCapManagersManagerMergesEntries(t *testing.T) { func TestCapUpdateManagers(t *testing.T) { f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", - func(m fieldmanager.Manager) fieldmanager.Manager { - return fieldmanager.NewCapManagersManager(m, 3) + func(m internal.Manager) internal.Manager { + return internal.NewCapManagersManager(m, 3) }) set := func(fields ...string) *metav1.FieldsV1 { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager.go new file mode 100644 index 00000000000..74cf4cb4f6e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager.go @@ -0,0 +1,206 @@ +/* +Copyright 2022 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 internal + +import ( + "fmt" + "reflect" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" + "sigs.k8s.io/structured-merge-diff/v4/merge" +) + +// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates +// if the number of update managers exceeds this, the oldest entries will be merged until the number is below the maximum. +// TODO(jennybuckley): Determine if this is really the best value. Ideally we wouldn't unnecessarily merge too many entries. +const DefaultMaxUpdateManagers int = 10 + +// DefaultTrackOnCreateProbability defines the default probability that the field management of an object +// starts being tracked from the object's creation, instead of from the first time the object is applied to. +const DefaultTrackOnCreateProbability float32 = 1 + +var atMostEverySecond = NewAtMostEvery(time.Second) + +// FieldManager updates the managed fields and merges applied +// configurations. +type FieldManager struct { + fieldManager Manager + subresource string +} + +// NewFieldManager creates a new FieldManager that decodes, manages, then re-encodes managedFields +// on update and apply requests. +func NewFieldManager(f Manager, subresource string) *FieldManager { + return &FieldManager{fieldManager: f, subresource: subresource} +} + +// newDefaultFieldManager is a helper function which wraps a Manager with certain default logic. +func NewDefaultFieldManager(f Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, subresource string) *FieldManager { + return NewFieldManager( + NewLastAppliedUpdater( + NewLastAppliedManager( + NewProbabilisticSkipNonAppliedManager( + NewCapManagersManager( + NewBuildManagerInfoManager( + NewManagedFieldsUpdater( + NewStripMetaManager(f), + ), kind.GroupVersion(), subresource, + ), DefaultMaxUpdateManagers, + ), objectCreater, kind, DefaultTrackOnCreateProbability, + ), typeConverter, objectConverter, kind.GroupVersion()), + ), subresource, + ) +} + +func decodeLiveOrNew(liveObj, newObj runtime.Object, ignoreManagedFieldsFromRequestObject bool) (Managed, error) { + liveAccessor, err := meta.Accessor(liveObj) + if err != nil { + return nil, err + } + + // We take the managedFields of the live object in case the request tries to + // manually set managedFields via a subresource. + if ignoreManagedFieldsFromRequestObject { + return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields())) + } + + // If the object doesn't have metadata, we should just return without trying to + // set the managedFields at all, so creates/updates/patches will work normally. + newAccessor, err := meta.Accessor(newObj) + if err != nil { + return nil, err + } + + if isResetManagedFields(newAccessor.GetManagedFields()) { + return NewEmptyManaged(), nil + } + + // If the managed field is empty or we failed to decode it, + // let's try the live object. This is to prevent clients who + // don't understand managedFields from deleting it accidentally. + managed, err := DecodeManagedFields(newAccessor.GetManagedFields()) + if err != nil || len(managed.Fields()) == 0 { + return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields())) + } + return managed, nil +} + +func emptyManagedFieldsOnErr(managed Managed, err error) (Managed, error) { + if err != nil { + return NewEmptyManaged(), nil + } + return managed, nil +} + +// Update is used when the object has already been merged (non-apply +// use-case), and simply updates the managed fields in the output +// object. +func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (object runtime.Object, err error) { + // First try to decode the managed fields provided in the update, + // This is necessary to allow directly updating managed fields. + isSubresource := f.subresource != "" + managed, err := decodeLiveOrNew(liveObj, newObj, isSubresource) + if err != nil { + return newObj, nil + } + + RemoveObjectManagedFields(newObj) + + if object, managed, err = f.fieldManager.Update(liveObj, newObj, managed, manager); err != nil { + return nil, err + } + + if err = EncodeObjectManagedFields(object, managed); err != nil { + return nil, fmt.Errorf("failed to encode managed fields: %v", err) + } + + return object, nil +} + +// UpdateNoErrors is the same as Update, but it will not return +// errors. If an error happens, the object is returned with +// managedFields cleared. +func (f *FieldManager) UpdateNoErrors(liveObj, newObj runtime.Object, manager string) runtime.Object { + obj, err := f.Update(liveObj, newObj, manager) + if err != nil { + atMostEverySecond.Do(func() { + ns, name := "unknown", "unknown" + if accessor, err := meta.Accessor(newObj); err == nil { + ns = accessor.GetNamespace() + name = accessor.GetName() + } + + klog.ErrorS(err, "[SHOULD NOT HAPPEN] failed to update managedFields", "VersionKind", + newObj.GetObjectKind().GroupVersionKind(), "namespace", ns, "name", name) + }) + // Explicitly remove managedFields on failure, so that + // we can't have garbage in it. + RemoveObjectManagedFields(newObj) + return newObj + } + return obj +} + +// Returns true if the managedFields indicate that the user is trying to +// reset the managedFields, i.e. if the list is non-nil but empty, or if +// the list has one empty item. +func isResetManagedFields(managedFields []metav1.ManagedFieldsEntry) bool { + if len(managedFields) == 0 { + return managedFields != nil + } + + if len(managedFields) == 1 { + return reflect.DeepEqual(managedFields[0], metav1.ManagedFieldsEntry{}) + } + + return false +} + +// Apply is used when server-side apply is called, as it merges the +// object and updates the managed fields. +func (f *FieldManager) Apply(liveObj, appliedObj runtime.Object, manager string, force bool) (object runtime.Object, err error) { + // If the object doesn't have metadata, apply isn't allowed. + accessor, err := meta.Accessor(liveObj) + if err != nil { + return nil, fmt.Errorf("couldn't get accessor: %v", err) + } + + // Decode the managed fields in the live object, since it isn't allowed in the patch. + managed, err := DecodeManagedFields(accessor.GetManagedFields()) + if err != nil { + return nil, fmt.Errorf("failed to decode managed fields: %v", err) + } + + object, managed, err = f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force) + if err != nil { + if conflicts, ok := err.(merge.Conflicts); ok { + return nil, NewConflictError(conflicts) + } + return nil, err + } + + if err = EncodeObjectManagedFields(object, managed); err != nil { + return nil, fmt.Errorf("failed to encode managed fields: %v", err) + } + + return object, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go new file mode 100644 index 00000000000..f8edf6913dc --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fieldmanager_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2019 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 internal_test + +import ( + "encoding/json" + "io/ioutil" + "path/filepath" + "strings" + + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +var fakeTypeConverter = func() internal.TypeConverter { + data, err := ioutil.ReadFile(filepath.Join( + strings.Repeat(".."+string(filepath.Separator), 9), + "api", "openapi-spec", "swagger.json")) + if err != nil { + panic(err) + } + spec := spec.Swagger{} + if err := json.Unmarshal(data, &spec); err != nil { + panic(err) + } + typeConverter, err := internal.NewTypeConverter(&spec, false) + if err != nil { + panic(err) + } + return typeConverter +}() + +var testTypeConverter = func() internal.TypeConverter { + data, err := ioutil.ReadFile(filepath.Join("testdata", "swagger.json")) + if err != nil { + panic(err) + } + spec := spec.Swagger{} + if err := json.Unmarshal(data, &spec); err != nil { + panic(err) + } + typeConverter, err := internal.NewTypeConverter(&spec, false) + if err != nil { + panic(err) + } + return typeConverter +}() diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedmanager.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedmanager.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedmanager.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedmanager.go index 4b07d462a22..88675630aae 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedmanager.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedmanager.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "encoding/json" diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedmanager_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedmanager_test.go similarity index 95% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedmanager_test.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedmanager_test.go index cc7cf935100..61f4055e9c7 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedmanager_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedmanager_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager_test +package internal_test import ( "fmt" @@ -24,7 +24,9 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + yamlutil "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" @@ -979,3 +981,27 @@ func testConflicts(t *testing.T, f fieldmanagertest.TestFieldManager, tests []te }) } } + +func yamlToJSON(y []byte) (string, error) { + obj := &unstructured.Unstructured{Object: map[string]interface{}{}} + if err := yaml.Unmarshal(y, &obj.Object); err != nil { + return "", fmt.Errorf("error decoding YAML: %v", err) + } + serialization, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) + if err != nil { + return "", fmt.Errorf("error encoding object: %v", err) + } + json, err := yamlutil.ToJSON(serialization) + if err != nil { + return "", fmt.Errorf("error converting to json: %v", err) + } + return string(json), nil +} + +func setLastAppliedFromEncoded(obj runtime.Object, lastApplied []byte) error { + lastAppliedJSON, err := yamlToJSON(lastApplied) + if err != nil { + return err + } + return internal.SetLastApplied(obj, lastAppliedJSON) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedupdater.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedupdater.go similarity index 95% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedupdater.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedupdater.go index e414e167f44..0d9320730a1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedupdater.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedupdater.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "fmt" @@ -23,7 +23,6 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" ) type lastAppliedUpdater struct { @@ -62,7 +61,7 @@ func (f *lastAppliedUpdater) Apply(liveObj, newObj runtime.Object, managed Manag if err != nil { return nil, nil, fmt.Errorf("failed to build last-applied annotation: %v", err) } - err = internal.SetLastApplied(liveObj, lastAppliedValue) + err = SetLastApplied(liveObj, lastAppliedValue) if err != nil { return nil, nil, fmt.Errorf("failed to set last-applied annotation: %v", err) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedupdater_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedupdater_test.go similarity index 88% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedupdater_test.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedupdater_test.go index dc9e7ff2455..c89f03c3b72 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/lastappliedupdater_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/lastappliedupdater_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager_test +package internal_test import ( "fmt" @@ -25,17 +25,18 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/yaml" ) func TestLastAppliedUpdater(t *testing.T) { f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("apps/v1", "Deployment"), "", - func(m fieldmanager.Manager) fieldmanager.Manager { - return fieldmanager.NewLastAppliedUpdater(m) + func(m internal.Manager) internal.Manager { + return internal.NewLastAppliedUpdater(m) }) originalLastApplied := `nonempty` @@ -191,8 +192,8 @@ func TestLargeLastApplied(t *testing.T) { t.Run(test.name, func(t *testing.T) { f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", - func(m fieldmanager.Manager) fieldmanager.Manager { - return fieldmanager.NewLastAppliedUpdater(m) + func(m internal.Manager) internal.Manager { + return internal.NewLastAppliedUpdater(m) }) if err := f.Apply(test.oldObject, "kubectl", false); err != nil { @@ -226,3 +227,20 @@ func TestLargeLastApplied(t *testing.T) { }) } } + +func getLastApplied(obj runtime.Object) (string, error) { + accessor := meta.NewAccessor() + annotations, err := accessor.Annotations(obj) + if err != nil { + return "", fmt.Errorf("failed to access annotations: %v", err) + } + if annotations == nil { + return "", fmt.Errorf("no annotations on obj: %v", obj) + } + + lastApplied, ok := annotations[corev1.LastAppliedConfigAnnotation] + if !ok { + return "", fmt.Errorf("expected last applied annotation, but got none for object: %v", obj) + } + return lastApplied, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/managedfieldsupdater.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfieldsupdater.go similarity index 95% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/managedfieldsupdater.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfieldsupdater.go index 412443a6c4e..376eed6b207 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/managedfieldsupdater.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfieldsupdater.go @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) @@ -77,7 +76,7 @@ func (f *managedFieldsUpdater) Apply(liveObj, appliedObj runtime.Object, managed managed.Times()[fieldManager] = &metav1.Time{Time: time.Now().UTC()} } else { object = liveObj.DeepCopyObject() - internal.RemoveObjectManagedFields(object) + RemoveObjectManagedFields(object) } return object, managed, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/managedfieldsupdater_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfieldsupdater_test.go similarity index 96% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/managedfieldsupdater_test.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfieldsupdater_test.go index 27b5a1f23cb..9d9683b23b7 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/managedfieldsupdater_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfieldsupdater_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager_test +package internal_test import ( "fmt" @@ -28,7 +28,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/yaml" @@ -428,11 +427,11 @@ func TestTakingOverManagedFieldsDuringApplyDoesNotModifyPreviousManagerTime(t *t type NoopManager struct{} -func (NoopManager) Apply(liveObj, appliedObj runtime.Object, managed fieldmanager.Managed, fieldManager string, force bool) (runtime.Object, fieldmanager.Managed, error) { +func (NoopManager) Apply(liveObj, appliedObj runtime.Object, managed internal.Managed, fieldManager string, force bool) (runtime.Object, internal.Managed, error) { return nil, managed, nil } -func (NoopManager) Update(liveObj, newObj runtime.Object, managed fieldmanager.Managed, manager string) (runtime.Object, fieldmanager.Managed, error) { +func (NoopManager) Update(liveObj, newObj runtime.Object, managed internal.Managed, manager string) (runtime.Object, internal.Managed, error) { return nil, nil, nil } @@ -498,12 +497,12 @@ func TestNilNewObjectReplacedWithDeepCopyExcludingManagedFields(t *testing.T) { } // Decode the managed fields in the live object, since it isn't allowed in the patch. - managed, err := fieldmanager.DecodeManagedFields(accessor.GetManagedFields()) + managed, err := internal.DecodeManagedFields(accessor.GetManagedFields()) if err != nil { t.Fatalf("failed to decode managed fields: %v", err) } - updater := fieldmanager.NewManagedFieldsUpdater(NoopManager{}) + updater := internal.NewManagedFieldsUpdater(NoopManager{}) newObject, _, err := updater.Apply(obj, obj.DeepCopyObject(), managed, "some_manager", false) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/manager.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/manager.go new file mode 100644 index 00000000000..053936103d7 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/manager.go @@ -0,0 +1,52 @@ +/* +Copyright 2022 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 internal + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" +) + +// Managed groups a fieldpath.ManagedFields together with the timestamps associated with each operation. +type Managed interface { + // Fields gets the fieldpath.ManagedFields. + Fields() fieldpath.ManagedFields + + // Times gets the timestamps associated with each operation. + Times() map[string]*metav1.Time +} + +// Manager updates the managed fields and merges applied configurations. +type Manager interface { + // Update is used when the object has already been merged (non-apply + // use-case), and simply updates the managed fields in the output + // object. + // * `liveObj` is not mutated by this function + // * `newObj` may be mutated by this function + // Returns the new object with managedFields removed, and the object's new + // proposed managedFields separately. + Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) + + // Apply is used when server-side apply is called, as it merges the + // object and updates the managed fields. + // * `liveObj` is not mutated by this function + // * `newObj` may be mutated by this function + // Returns the new object with managedFields removed, and the object's new + // proposed managedFields separately. + Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/skipnonapplied.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/skipnonapplied.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/skipnonapplied.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/skipnonapplied.go index a8c34ad6529..6b281ec1e57 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/skipnonapplied.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/skipnonapplied.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "fmt" diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/skipnonapplied_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/skipnonapplied_test.go similarity index 91% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/skipnonapplied_test.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/skipnonapplied_test.go index 1373676706f..c5723472d69 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/skipnonapplied_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/skipnonapplied_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager_test +package internal_test import ( "strings" @@ -24,14 +24,14 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/yaml" ) func TestNoUpdateBeforeFirstApply(t *testing.T) { - f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", func(m fieldmanager.Manager) fieldmanager.Manager { - return fieldmanager.NewSkipNonAppliedManager( + f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", func(m internal.Manager) internal.Manager { + return internal.NewSkipNonAppliedManager( m, fieldmanagertest.NewFakeObjectCreater(), schema.FromAPIVersionAndKind("v1", "Pod"), @@ -70,8 +70,8 @@ func TestNoUpdateBeforeFirstApply(t *testing.T) { } func TestUpdateBeforeFirstApply(t *testing.T) { - f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", func(m fieldmanager.Manager) fieldmanager.Manager { - return fieldmanager.NewSkipNonAppliedManager( + f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", func(m internal.Manager) internal.Manager { + return internal.NewSkipNonAppliedManager( m, fieldmanagertest.NewFakeObjectCreater(), schema.FromAPIVersionAndKind("v1", "Pod"), diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/stripmeta.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/stripmeta.go similarity index 99% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/stripmeta.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/stripmeta.go index 1460d9c8021..9b61f3a6f35 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/stripmeta.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/stripmeta.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "fmt" diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/structuredmerge.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/structuredmerge.go similarity index 97% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/structuredmerge.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/structuredmerge.go index 213988e23c2..eb5598ac3bf 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/structuredmerge.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/structuredmerge.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "fmt" @@ -23,7 +23,6 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" "sigs.k8s.io/structured-merge-diff/v4/merge" ) @@ -108,7 +107,7 @@ func (f *structuredMergeManager) Update(liveObj, newObj runtime.Object, managed if err != nil { return nil, nil, fmt.Errorf("failed to update ManagedFields (%v): %v", objectGVKNN(newObjVersioned), err) } - managed = internal.NewManaged(managedFields, managed.Times()) + managed = NewManaged(managedFields, managed.Times()) return newObj, managed, nil } @@ -151,7 +150,7 @@ func (f *structuredMergeManager) Apply(liveObj, patchObj runtime.Object, managed if err != nil { return nil, nil, err } - managed = internal.NewManaged(managedFields, managed.Times()) + managed = NewManaged(managedFields, managed.Times()) if newObjTyped == nil { return nil, managed, nil diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/testdata/swagger.json b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/testdata/swagger.json similarity index 100% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/testdata/swagger.json rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/testdata/swagger.json 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 new file mode 100644 index 00000000000..0ffeac6f530 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go @@ -0,0 +1,116 @@ +/* +Copyright 2022 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 internal + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/managedfields" + utilopenapi "k8s.io/apiserver/pkg/util/openapi" + "k8s.io/kube-openapi/pkg/validation/spec" + "sigs.k8s.io/structured-merge-diff/v4/typed" + "sigs.k8s.io/structured-merge-diff/v4/value" +) + +// TypeConverter allows you to convert from runtime.Object to +// typed.TypedValue and the other way around. +type TypeConverter interface { + ObjectToTyped(runtime.Object) (*typed.TypedValue, error) + TypedToObject(*typed.TypedValue) (runtime.Object, error) +} + +type typeConverter struct { + parser *managedfields.GvkParser +} + +var _ TypeConverter = &typeConverter{} + +// NewTypeConverter builds a TypeConverter from a proto.Models. This +// will automatically find the proper version of the object, and the +// corresponding schema information. +func NewTypeConverter(openapiSpec *spec.Swagger, preserveUnknownFields bool) (TypeConverter, error) { + models, err := utilopenapi.ToProtoModels(openapiSpec) + if err != nil { + return nil, err + } + parser, err := managedfields.NewGVKParser(models, preserveUnknownFields) + if err != nil { + return nil, err + } + return &typeConverter{parser: parser}, nil +} + +func (c *typeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) { + gvk := obj.GetObjectKind().GroupVersionKind() + t := c.parser.Type(gvk) + if t == nil { + return nil, NewNoCorrespondingTypeError(gvk) + } + switch o := obj.(type) { + case *unstructured.Unstructured: + return t.FromUnstructured(o.UnstructuredContent()) + default: + return t.FromStructured(obj) + } +} + +func (c *typeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) { + return valueToObject(value.AsValue()) +} + +// 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). +// TODO(jennybuckley): Use the schema provided by a CRD if it exists. +type deducedTypeConverter struct{} + +func NewDeducedTypeConverter() TypeConverter { + return deducedTypeConverter{} +} + +// ObjectToTyped converts an object into a TypedValue with a "deduced type". +func (deducedTypeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) { + switch o := obj.(type) { + case *unstructured.Unstructured: + return typed.DeducedParseableType.FromUnstructured(o.UnstructuredContent()) + default: + return typed.DeducedParseableType.FromStructured(obj) + } +} + +// 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()) +} + +func valueToObject(val value.Value) (runtime.Object, error) { + vu := val.Unstructured() + switch o := vu.(type) { + case map[string]interface{}: + return &unstructured.Unstructured{Object: o}, nil + default: + return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go similarity index 83% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter_test.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go index a51abd8cc95..83197ea886a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go @@ -14,13 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager_test +package internal_test import ( - "encoding/json" "fmt" - "io/ioutil" - "path/filepath" "reflect" "testing" @@ -28,28 +25,11 @@ import ( "sigs.k8s.io/yaml" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" - "k8s.io/kube-openapi/pkg/validation/spec" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" ) -var testTypeConverter = func() fieldmanager.TypeConverter { - data, err := ioutil.ReadFile(filepath.Join("testdata", "swagger.json")) - if err != nil { - panic(err) - } - spec := spec.Swagger{} - if err := json.Unmarshal(data, &spec); err != nil { - panic(err) - } - typeConverter, err := fieldmanager.NewTypeConverter(&spec, false) - if err != nil { - panic(err) - } - return typeConverter -}() - func TestTypeConverter(t *testing.T) { - dtc := fieldmanager.DeducedTypeConverter{} + dtc := internal.NewDeducedTypeConverter() testCases := []struct { name string @@ -126,7 +106,7 @@ spec: } } -func testObjectToTyped(t *testing.T, tc fieldmanager.TypeConverter, y string) { +func testObjectToTyped(t *testing.T, tc internal.TypeConverter, y string) { obj := &unstructured.Unstructured{Object: map[string]interface{}{}} if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil { t.Fatalf("Failed to parse yaml object: %v", err) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/versionconverter.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter.go similarity index 87% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/versionconverter.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter.go index 477e92f796e..45855fa4ca2 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/versionconverter.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter.go @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( + "fmt" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" @@ -99,3 +101,23 @@ func (v *versionConverter) Convert(object *typed.TypedValue, version fieldpath.A func (v *versionConverter) IsMissingVersionError(err error) bool { return runtime.IsNotRegisteredError(err) || isNoCorrespondingTypeError(err) } + +type noCorrespondingTypeErr struct { + gvk schema.GroupVersionKind +} + +func NewNoCorrespondingTypeError(gvk schema.GroupVersionKind) error { + return &noCorrespondingTypeErr{gvk: gvk} +} + +func (k *noCorrespondingTypeErr) Error() string { + return fmt.Sprintf("no corresponding type for %v", k.gvk) +} + +func isNoCorrespondingTypeError(err error) bool { + if err == nil { + return false + } + _, ok := err.(*noCorrespondingTypeErr) + return ok +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/versionconverter_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter_test.go similarity index 87% rename from staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/versionconverter_test.go rename to staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter_test.go index 3eb90ccbb8b..19e4476cb02 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/versionconverter_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package fieldmanager +package internal import ( "encoding/json" @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) -var testSchema = func() *spec.Swagger { +var testTypeConverter = func() TypeConverter { data, err := ioutil.ReadFile(filepath.Join("testdata", "swagger.json")) if err != nil { panic(err) @@ -40,22 +40,22 @@ var testSchema = func() *spec.Swagger { if err := json.Unmarshal(data, &spec); err != nil { panic(err) } - return &spec + typeConverter, err := NewTypeConverter(&spec, false) + if err != nil { + panic(err) + } + return typeConverter }() // TestVersionConverter tests the version converter func TestVersionConverter(t *testing.T) { - tc, err := NewTypeConverter(testSchema, false) - if err != nil { - t.Fatalf("Failed to build TypeConverter: %v", err) - } oc := fakeObjectConvertorForTestSchema{ gvkForVersion("v1beta1"): objForGroupVersion("apps/v1beta1"), gvkForVersion("v1"): objForGroupVersion("apps/v1"), } - vc := newVersionConverter(tc, oc, schema.GroupVersion{Group: "apps", Version: runtime.APIVersionInternal}) + vc := newVersionConverter(testTypeConverter, oc, schema.GroupVersion{Group: "apps", Version: runtime.APIVersionInternal}) - input, err := tc.ObjectToTyped(objForGroupVersion("apps/v1beta1")) + input, err := testTypeConverter.ObjectToTyped(objForGroupVersion("apps/v1beta1")) if err != nil { t.Fatalf("error creating converting input object to a typed value: %v", err) } @@ -64,7 +64,7 @@ func TestVersionConverter(t *testing.T) { if err != nil { t.Fatalf("expected err to be nil but got %v", err) } - actual, err := tc.TypedToObject(output) + actual, err := testTypeConverter.TypedToObject(output) if err != nil { t.Fatalf("error converting output typed value to an object %v", err) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/scalehandler.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/scalehandler.go index d9844990c29..655bf91d1e1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/scalehandler.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/scalehandler.go @@ -60,7 +60,7 @@ func NewScaleHandler(parentEntries []metav1.ManagedFieldsEntry, groupVersion sch // 2. Replicas path of the main resource is transformed to the replicas path of // the scale subresource func (h *ScaleHandler) ToSubresource() ([]metav1.ManagedFieldsEntry, error) { - managed, err := DecodeManagedFields(h.parentEntries) + managed, err := internal.DecodeManagedFields(h.parentEntries) if err != nil { return nil, err } @@ -92,13 +92,13 @@ func (h *ScaleHandler) ToSubresource() ([]metav1.ManagedFieldsEntry, error) { // ToParent merges `scaleEntries` with the entries of the main resource and // transforms them accordingly func (h *ScaleHandler) ToParent(scaleEntries []metav1.ManagedFieldsEntry) ([]metav1.ManagedFieldsEntry, error) { - decodedParentEntries, err := DecodeManagedFields(h.parentEntries) + decodedParentEntries, err := internal.DecodeManagedFields(h.parentEntries) if err != nil { return nil, err } parentFields := decodedParentEntries.Fields() - decodedScaleEntries, err := DecodeManagedFields(scaleEntries) + decodedScaleEntries, err := internal.DecodeManagedFields(scaleEntries) if err != nil { return nil, err } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go index 9dc42161493..62a4c353664 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go @@ -17,119 +17,30 @@ limitations under the License. package fieldmanager import ( - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/managedfields" - utilopenapi "k8s.io/apiserver/pkg/util/openapi" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" "k8s.io/kube-openapi/pkg/validation/spec" - "sigs.k8s.io/structured-merge-diff/v4/typed" - "sigs.k8s.io/structured-merge-diff/v4/value" ) // TypeConverter allows you to convert from runtime.Object to // typed.TypedValue and the other way around. -type TypeConverter interface { - ObjectToTyped(runtime.Object) (*typed.TypedValue, error) - TypedToObject(*typed.TypedValue) (runtime.Object, error) -} +type TypeConverter = internal.TypeConverter -// 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 +// NewDeducedTypeConverter creates 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). // TODO(jennybuckley): Use the schema provided by a CRD if it exists. -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) { - switch o := obj.(type) { - case *unstructured.Unstructured: - return typed.DeducedParseableType.FromUnstructured(o.UnstructuredContent()) - default: - return typed.DeducedParseableType.FromStructured(obj) - } +func NewDeducedTypeConverter() TypeConverter { + return internal.NewDeducedTypeConverter() } -// 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 *managedfields.GvkParser -} - -var _ TypeConverter = &typeConverter{} - -// NewTypeConverter builds a TypeConverter from a spec.Swagger. This +// NewTypeConverter builds a TypeConverter from a proto.Models. This // will automatically find the proper version of the object, and the // corresponding schema information. func NewTypeConverter(openapiSpec *spec.Swagger, preserveUnknownFields bool) (TypeConverter, error) { - models, err := utilopenapi.ToProtoModels(openapiSpec) - if err != nil { - return nil, err - } - parser, err := managedfields.NewGVKParser(models, preserveUnknownFields) - if err != nil { - return nil, err - } - return &typeConverter{parser: parser}, nil -} - -func (c *typeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) { - gvk := obj.GetObjectKind().GroupVersionKind() - t := c.parser.Type(gvk) - if t == nil { - return nil, newNoCorrespondingTypeError(gvk) - } - switch o := obj.(type) { - case *unstructured.Unstructured: - return t.FromUnstructured(o.UnstructuredContent()) - default: - return t.FromStructured(obj) - } -} - -func (c *typeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) { - return valueToObject(value.AsValue()) -} - -func valueToObject(val value.Value) (runtime.Object, error) { - vu := val.Unstructured() - switch o := vu.(type) { - case map[string]interface{}: - return &unstructured.Unstructured{Object: o}, nil - default: - return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu) - } -} - -type noCorrespondingTypeErr struct { - gvk schema.GroupVersionKind -} - -func newNoCorrespondingTypeError(gvk schema.GroupVersionKind) error { - return &noCorrespondingTypeErr{gvk: gvk} -} - -func (k *noCorrespondingTypeErr) Error() string { - return fmt.Sprintf("no corresponding type for %v", k.gvk) -} - -func isNoCorrespondingTypeError(err error) bool { - if err == nil { - return false - } - _, ok := err.(*noCorrespondingTypeErr) - return ok + return internal.NewTypeConverter(openapiSpec, preserveUnknownFields) } diff --git a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go index 476eae690b1..684805ac55c 100644 --- a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go +++ b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go @@ -169,8 +169,9 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { // validate against both decoding and validation to make sure we use the hardest rule between the both to reset // with decoding being as strict as it gets, only using it should be enough in admission func validateManagedFieldsAndDecode(managedFields []metav1.ManagedFieldsEntry) error { - if _, err := fieldmanager.DecodeManagedFields(managedFields); err != nil { + if err := fieldmanager.ValidateManagedFields(managedFields); err != nil { return err + } validationErrs := v1validation.ValidateManagedFields(managedFields, field.NewPath("metadata").Child("managedFields")) return validationErrs.ToAggregate() diff --git a/test/integration/apiserver/timestamp_transformer_test.go b/test/integration/apiserver/timestamp_transformer_test.go index bdc564e7da8..345c9158129 100644 --- a/test/integration/apiserver/timestamp_transformer_test.go +++ b/test/integration/apiserver/timestamp_transformer_test.go @@ -34,7 +34,7 @@ import ( ) func convertToUnstructured(b *testing.B, obj runtime.Object) runtime.Object { - converter := fieldmanager.DeducedTypeConverter{} + converter := fieldmanager.NewDeducedTypeConverter() typed, err := converter.ObjectToTyped(obj) require.NoError(b, err) res, err := converter.TypedToObject(typed) From ad65b25cc33b7323d6651407944fe74d938ac42a Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Thu, 15 Dec 2022 16:42:40 -0800 Subject: [PATCH 2/2] fieldmanager: Remove obsolete comment --- .../handlers/fieldmanager/internal/typeconverter.go | 8 ++------ .../pkg/endpoints/handlers/fieldmanager/typeconverter.go | 4 ---- 2 files changed, 2 insertions(+), 10 deletions(-) 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 0ffeac6f530..3ff7507b32c 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 @@ -74,17 +74,13 @@ func (c *typeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, return valueToObject(value.AsValue()) } +type deducedTypeConverter struct{} + // 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). -// TODO(jennybuckley): Use the schema provided by a CRD if it exists. -type deducedTypeConverter struct{} - func NewDeducedTypeConverter() TypeConverter { return deducedTypeConverter{} } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go index 62a4c353664..0aa97f7e790 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/typeconverter.go @@ -30,10 +30,6 @@ type TypeConverter = internal.TypeConverter // 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). -// TODO(jennybuckley): Use the schema provided by a CRD if it exists. func NewDeducedTypeConverter() TypeConverter { return internal.NewDeducedTypeConverter() }