From ca99bda7e3244579244ed18996c1a7b2bbcc117e Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Mon, 22 Jun 2020 12:03:49 -0700 Subject: [PATCH 1/5] Remove fields when unset from applied configuration if there are no other owners --- merge/leaf_test.go | 9 ++------- merge/update.go | 10 +++++----- typed/remove.go | 7 +++---- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/merge/leaf_test.go b/merge/leaf_test.go index 344bef11..c38aa990 100644 --- a/merge/leaf_test.go +++ b/merge/leaf_test.go @@ -345,7 +345,7 @@ func TestUpdateLeaf(t *testing.T) { ), }, }, - "apply_twice_dangling": { + "apply_twice_remove": { Ops: []Operation{ Apply{ Manager: "default", @@ -365,9 +365,7 @@ func TestUpdateLeaf(t *testing.T) { }, }, Object: ` - numeric: 1 string: "new string" - bool: false `, APIVersion: "v1", Managed: fieldpath.ManagedFields{ @@ -380,7 +378,7 @@ func TestUpdateLeaf(t *testing.T) { ), }, }, - "apply_twice_dangling_different_version": { + "apply_twice_remove_different_version": { Ops: []Operation{ Apply{ Manager: "default", @@ -400,9 +398,7 @@ func TestUpdateLeaf(t *testing.T) { }, }, Object: ` - numeric: 1 string: "new string" - bool: false `, APIVersion: "v1", Managed: fieldpath.ManagedFields{ @@ -462,7 +458,6 @@ func TestUpdateLeaf(t *testing.T) { }, }, Object: ` - string: "string" `, APIVersion: "v1", Managed: fieldpath.ManagedFields{}, diff --git a/merge/update.go b/merge/update.go index edf8eee3..6193743f 100644 --- a/merge/update.go +++ b/merge/update.go @@ -212,7 +212,7 @@ func shallowCopyManagers(managers fieldpath.ManagedFields) fieldpath.ManagedFiel return newManagers } -// prune will remove a list or map item, iff: +// prune will remove a field, list or map item, iff: // * applyingManager applied it last time // * applyingManager didn't apply it this time // * no other applier claims to manage it @@ -240,7 +240,7 @@ func (s *Updater) prune(merged *typed.TypedValue, managers fieldpath.ManagedFiel return s.Converter.Convert(pruned, managers[applyingManager].APIVersion()) } -// addBackOwnedItems adds back any list and map items that were removed by prune, +// addBackOwnedItems adds back any fields, list and map items that were removed by prune, // but other appliers (or the current applier's new config) claim to own. func (s *Updater) addBackOwnedItems(merged, pruned *typed.TypedValue, managedFields fieldpath.ManagedFields, applyingManager string) (*typed.TypedValue, error) { var err error @@ -281,9 +281,9 @@ func (s *Updater) addBackOwnedItems(merged, pruned *typed.TypedValue, managedFie return pruned, nil } -// addBackDanglingItems makes sure that the only items removed by prune are items that were -// previously owned by the currently applying manager. This will add back unowned items and items -// which are owned by Updaters that shouldn't be removed. +// addBackDanglingItems makes sure that the fields list and map items removed by prune were +// previously owned by the currently applying manager. This will add back fields list and map items +// that are unowned or that are owned by Updaters and shouldn't be removed. func (s *Updater) addBackDanglingItems(merged, pruned *typed.TypedValue, lastSet fieldpath.VersionedSet) (*typed.TypedValue, error) { convertedPruned, err := s.Converter.Convert(pruned, lastSet.APIVersion()) if err != nil { diff --git a/typed/remove.go b/typed/remove.go index f30e02a6..cfa6365a 100644 --- a/typed/remove.go +++ b/typed/remove.go @@ -95,10 +95,9 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { fieldType := t.ElementType if ft, ok := fieldTypes[k]; ok { fieldType = ft - } else { - if w.toRemove.Has(path) { - return true - } + } + if w.toRemove.Has(path) { + return true } if subset := w.toRemove.WithPrefix(pe); !subset.Empty() { val = removeItemsWithSchema(val, subset, w.schema, fieldType) From 1ce4a774aa0ca33e3f7f844bb9c690980e769ad7 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Thu, 25 Jun 2020 23:37:57 -0700 Subject: [PATCH 2/5] Add field unsetting tests --- merge/multiple_appliers_test.go | 342 ++++++++++++++++++++++++++++++++ typed/typed.go | 5 + 2 files changed, 347 insertions(+) diff --git a/merge/multiple_appliers_test.go b/merge/multiple_appliers_test.go index 0571ae50..c44e70d5 100644 --- a/merge/multiple_appliers_test.go +++ b/merge/multiple_appliers_test.go @@ -27,6 +27,7 @@ import ( . "sigs.k8s.io/structured-merge-diff/v3/internal/fixture" "sigs.k8s.io/structured-merge-diff/v3/merge" "sigs.k8s.io/structured-merge-diff/v3/typed" + "sigs.k8s.io/structured-merge-diff/v3/value" ) func TestMultipleAppliersSet(t *testing.T) { @@ -243,6 +244,300 @@ func TestMultipleAppliersSet(t *testing.T) { } } +var structMultiversionParser = func() Parser { + parser, err := typed.NewParser(`types: +- name: v1 + map: + fields: + - name: struct + type: + namedType: struct + - name: version + type: + scalar: string +- name: struct + map: + fields: + - name: name + type: + scalar: string + - name: scalarField_v1 + type: + scalar: string + - name: complexField_v1 + type: + namedType: complex +- name: complex + map: + fields: + - name: name + type: + scalar: string +- name: v2 + map: + fields: + - name: struct + type: + namedType: struct_v2 + - name: version + type: + scalar: string +- name: struct_v2 + map: + fields: + - name: name + type: + scalar: string + - name: scalarField_v2 + type: + scalar: string + - name: complexField_v2 + type: + namedType: complex_v2 +- name: complex_v2 + map: + fields: + - name: name + type: + scalar: string +- name: v3 + map: + fields: + - name: struct + type: + namedType: struct_v3 + - name: version + type: + scalar: string +- name: struct_v3 + map: + fields: + - name: name + type: + scalar: string + - name: scalarField_v3 + type: + scalar: string + - name: complexField_v3 + type: + namedType: complex_v3 +- name: complex_v3 + map: + fields: + - name: name + type: + scalar: string +`) + if err != nil { + panic(err) + } + return parser +}() + +func TestMultipleAppliersFieldUnsetting(t *testing.T) { + versions := []fieldpath.APIVersion{"v1", "v2", "v3"} + for _, v1 := range versions { + for _, v2 := range versions { + for _, v3 := range versions { + t.Run(fmt.Sprintf("%s-%s-%s", v1, v2, v3), func(t *testing.T) { + testMultipleAppliersFieldUnsetting(t, v1, v2, v3) + }) + } + } + } +} + +func testMultipleAppliersFieldUnsetting(t *testing.T, v1, v2, v3 fieldpath.APIVersion) { + tests := map[string]TestCase{ + "unset_scalar_sole_owner": { + Ops: []Operation{ + Apply{ + Manager: "apply-one", + APIVersion: v1, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v1)), + }, + Apply{ + Manager: "apply-one", + APIVersion: v2, + Object: ` + struct: + name: a + `, + }, + }, + Object: ` + struct: + name: a + `, + APIVersion: v3, + Managed: fieldpath.ManagedFields{ + "apply-one": fieldpath.NewVersionedSet( + _NS( + _P("struct", "name"), + ), + v2, + false, + ), + }, + }, + "unset_scalar_shared_owner": { + Ops: []Operation{ + Apply{ + Manager: "apply-one", + APIVersion: v1, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v1)), + }, + Apply{ + Manager: "apply-two", + APIVersion: v2, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + scalarField_%s: a + `, v2)), + }, + Apply{ + Manager: "apply-one", + APIVersion: v3, + Object: ` + struct: + name: a + `, + }, + }, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v3)), + APIVersion: v3, + Managed: fieldpath.ManagedFields{ + "apply-one": fieldpath.NewVersionedSet( + _NS( + _P("struct", "name"), + ), + v3, + true, + ), + "apply-two": fieldpath.NewVersionedSet( + _NS( + _P("struct", fmt.Sprintf("scalarField_%s", v2)), + ), + v2, + false, + ), + }, + }, + "unset_complex_sole_owner": { + Ops: []Operation{ + Apply{ + Manager: "apply-one", + APIVersion: v1, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + complexField_%s: + name: b + `, v1)), + }, + Apply{ + Manager: "apply-one", + APIVersion: v2, + Object: ` + struct: + name: a + `, + }, + }, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + complexField_%s: null + `, v3)), + APIVersion: v3, + Managed: fieldpath.ManagedFields{ + "apply-one": fieldpath.NewVersionedSet( + _NS( + _P("struct", "name"), + ), + v2, + false, + ), + }, + }, + "unset_complex_shared_owner": { + Ops: []Operation{ + Apply{ + Manager: "apply-one", + APIVersion: v1, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + complexField_%s: + name: b + `, v1)), + }, + Apply{ + Manager: "apply-two", + APIVersion: v2, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + complexField_%s: + name: b + `, v2)), + }, + Apply{ + Manager: "apply-one", + APIVersion: v3, + Object: ` + struct: + name: a + `, + }, + }, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + complexField_%s: + name: b + `, v3)), + APIVersion: v3, + Managed: fieldpath.ManagedFields{ + "apply-one": fieldpath.NewVersionedSet( + _NS( + _P("struct", "name"), + ), + v3, + false, + ), + "apply-two": fieldpath.NewVersionedSet( + _NS( + _P("struct", fmt.Sprintf("complexField_%s", v2), "name"), + ), + v2, + false, + ), + }, + }, + } + + converter := renamingConverter{structMultiversionParser} + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if err := test.TestWithConverter(structMultiversionParser, converter); err != nil { + t.Fatal(err) + } + }) + } +} + func TestMultipleAppliersNestedType(t *testing.T) { tests := map[string]TestCase{ "remove_one_keep_one_with_two_sub_items": { @@ -1041,6 +1336,53 @@ func (r repeatingConverter) IsMissingVersionError(err error) bool { return err == missingVersionError } +// renamingConverter renames fields by substituting the version suffix of the field name. E.g. +// converting a map with a field named "name_v1" from v1 to v2 renames the field to "name_v2". +// Fields without a version suffix are not converted; they are the same in all versions. +// When parsing, this converter will look for the type by using the APIVersion of the +// object it's trying to parse. If trying to parse a "v1" object, a corresponding "v1" type +// should exist in the schema of the provided parser. +type renamingConverter struct { + parser Parser +} + +// Convert implements merge.Converter +func (r renamingConverter) Convert(v *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) { + inVersion := fieldpath.APIVersion(*v.TypeRef().NamedType) + outType := r.parser.Type(string(version)) + return outType.FromUnstructured(renameFields(v.AsValue(), string(inVersion), string(version))) +} + +func renameFields(v value.Value, oldSuffix, newSuffix string) interface{} { + if v.IsMap() { + out := map[string]interface{}{} + v.AsMap().Iterate(func(key string, value value.Value) bool { + if strings.HasSuffix(key, oldSuffix) { + out[strings.TrimSuffix(key, oldSuffix)+newSuffix] = renameFields(value, oldSuffix, newSuffix) + } else { + out[key] = renameFields(value, oldSuffix, newSuffix) + } + return true + }) + return out + } + if v.IsList() { + var out []interface{} + ri := v.AsList().Range() + for ri.Next() { + _, v := ri.Item() + out = append(out, renameFields(v, oldSuffix, newSuffix)) + } + return out + } + return v.Unstructured() +} + +// Convert implements merge.Converter +func (r renamingConverter) IsMissingVersionError(err error) bool { + return err == missingVersionError +} + func BenchmarkMultipleApplierRecursiveRealConversion(b *testing.B) { test := TestCase{ Ops: []Operation{ diff --git a/typed/typed.go b/typed/typed.go index 9a7ef9be..056a07de 100644 --- a/typed/typed.go +++ b/typed/typed.go @@ -61,6 +61,11 @@ type TypedValue struct { schema *schema.Schema } +// TypeRef is the type of the value. +func (tv TypedValue) TypeRef() schema.TypeRef { + return tv.typeRef +} + // AsValue removes the type from the TypedValue and only keeps the value. func (tv TypedValue) AsValue() value.Value { return tv.value From 085f5bfe506970c58fe6c1f6666ad985e6c00a82 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 30 Jun 2020 13:43:02 -0400 Subject: [PATCH 3/5] Apply suggestion Co-authored-by: Antoine Pelisse --- merge/leaf_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/merge/leaf_test.go b/merge/leaf_test.go index c38aa990..57b6dad3 100644 --- a/merge/leaf_test.go +++ b/merge/leaf_test.go @@ -378,6 +378,42 @@ func TestUpdateLeaf(t *testing.T) { ), }, }, + "update_apply_omits": { + Ops: []Operation{ + Apply{ + Manager: "default", + APIVersion: "v1", + Object: ` + numeric: 2 + `, + }, + Update{ + Manager: "controller", + APIVersion: "v1", + Object: ` + numeric: 1 + `, + }, + Apply{ + Manager: "default", + APIVersion: "v1", + Object: ``, + }, + }, + Object: ` + numeric: 1 + `, + APIVersion: "v1", + Managed: fieldpath.ManagedFields{ + "controller": fieldpath.NewVersionedSet( + _NS( + _P("numeric"), + ), + "v1", + false, + ), + }, + }, "apply_twice_remove_different_version": { Ops: []Operation{ Apply{ From 9f09075a4014683d312b17d9b25f5c106f9deb8f Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Tue, 30 Jun 2020 14:58:55 -0700 Subject: [PATCH 4/5] Change merge rules so that updaters to share ownership with appliers --- merge/multiple_appliers_test.go | 179 +++++++++++++++++++++++++++++++- merge/update.go | 10 +- 2 files changed, 180 insertions(+), 9 deletions(-) diff --git a/merge/multiple_appliers_test.go b/merge/multiple_appliers_test.go index c44e70d5..4b0f88b5 100644 --- a/merge/multiple_appliers_test.go +++ b/merge/multiple_appliers_test.go @@ -384,7 +384,7 @@ func testMultipleAppliersFieldUnsetting(t *testing.T, v1, v2, v3 fieldpath.APIVe ), }, }, - "unset_scalar_shared_owner": { + "unset_scalar_shared_with_applier": { Ops: []Operation{ Apply{ Manager: "apply-one", @@ -435,6 +435,104 @@ func testMultipleAppliersFieldUnsetting(t *testing.T, v1, v2, v3 fieldpath.APIVe ), }, }, + "unset_scalar_shared_with_updater": { + Ops: []Operation{ + Update{ + Manager: "updater", + APIVersion: v1, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v1)), + }, + Apply{ + Manager: "applier", + APIVersion: v2, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v2)), + }, + Apply{ + Manager: "applier", + APIVersion: v3, + Object: ` + struct: + name: a + `, + }, + }, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v3)), + APIVersion: v3, + Managed: fieldpath.ManagedFields{ + "updater": fieldpath.NewVersionedSet( + _NS( + _P("struct"), + _P("struct", "name"), + _P("struct", fmt.Sprintf("scalarField_%s", v1)), + ), + v1, + false, + ), + "applier": fieldpath.NewVersionedSet( + _NS( + _P("struct", "name"), + ), + v3, + true, + ), + }, + }, + "updater_claims_field": { + Ops: []Operation{ + Apply{ + Manager: "applier", + APIVersion: v1, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v1)), + }, + Update{ + Manager: "updater", + APIVersion: v2, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: b + `, v2)), + }, + }, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: b + `, v3)), + APIVersion: v3, + Managed: fieldpath.ManagedFields{ + "updater": fieldpath.NewVersionedSet( + _NS( + _P("struct", fmt.Sprintf("scalarField_%s", v2)), + ), + v2, + false, + ), + "applier": fieldpath.NewVersionedSet( + _NS( + _P("struct", "name"), + ), + v1, + true, + ), + }, + }, "unset_complex_sole_owner": { Ops: []Operation{ Apply{ @@ -472,7 +570,7 @@ func testMultipleAppliersFieldUnsetting(t *testing.T, v1, v2, v3 fieldpath.APIVe ), }, }, - "unset_complex_shared_owner": { + "unset_complex_shared_with_applier": { Ops: []Operation{ Apply{ Manager: "apply-one", @@ -1206,7 +1304,7 @@ func TestMultipleAppliersRealConversion(t *testing.T) { ), }, }, - "appliers_remove_from_controller_real_conversion": { + "applier_updater_shared_ownership_real_conversion": { Ops: []Operation{ Update{ Manager: "controller", @@ -1242,6 +1340,8 @@ func TestMultipleAppliersRealConversion(t *testing.T) { Object: ` mapOfMapsRecursive: aaa: + bbb: + ccc: ccc: `, APIVersion: "v3", @@ -1250,6 +1350,8 @@ func TestMultipleAppliersRealConversion(t *testing.T) { _NS( _P("mapOfMapsRecursive"), _P("mapOfMapsRecursive", "a"), + _P("mapOfMapsRecursive", "a", "b"), + _P("mapOfMapsRecursive", "a", "b", "c"), ), "v1", false, @@ -1275,6 +1377,77 @@ func TestMultipleAppliersRealConversion(t *testing.T) { } } +func TestMultipleAppliersFieldRenameConversions(t *testing.T) { + versions := []fieldpath.APIVersion{"v1", "v2", "v3"} + for _, v1 := range versions { + for _, v2 := range versions { + for _, v3 := range versions { + t.Run(fmt.Sprintf("%s-%s-%s", v1, v2, v3), func(t *testing.T) { + testMultipleAppliersFieldRenameConversions(t, v1, v2, v3) + }) + } + } + } +} + +func testMultipleAppliersFieldRenameConversions(t *testing.T, v1, v2, v3 fieldpath.APIVersion) { + tests := map[string]TestCase{ + "updater_claims_field": { + Ops: []Operation{ + Apply{ + Manager: "applier", + APIVersion: v1, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: a + `, v1)), + }, + Update{ + Manager: "updater", + APIVersion: v2, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: b + `, v2)), + }, + }, + Object: typed.YAMLObject(fmt.Sprintf(` + struct: + name: a + scalarField_%s: b + `, v3)), + APIVersion: v3, + Managed: fieldpath.ManagedFields{ + "updater": fieldpath.NewVersionedSet( + _NS( + _P("struct", fmt.Sprintf("scalarField_%s", v2)), + ), + v2, + false, + ), + "applier": fieldpath.NewVersionedSet( + _NS( + _P("struct", "name"), + ), + v1, + true, + ), + }, + }, + } + + converter := renamingConverter{structMultiversionParser} + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if err := test.TestWithConverter(structMultiversionParser, converter); err != nil { + t.Fatal(err) + } + }) + } +} + // repeatingConverter repeats a single letterkey v times, where v is the version. type repeatingConverter struct { parser Parser diff --git a/merge/update.go b/merge/update.go index 6193743f..7b88cbd8 100644 --- a/merge/update.go +++ b/merge/update.go @@ -241,17 +241,15 @@ func (s *Updater) prune(merged *typed.TypedValue, managers fieldpath.ManagedFiel } // addBackOwnedItems adds back any fields, list and map items that were removed by prune, -// but other appliers (or the current applier's new config) claim to own. +// but other appliers or updaters (or the current applier's new config) claim to own. func (s *Updater) addBackOwnedItems(merged, pruned *typed.TypedValue, managedFields fieldpath.ManagedFields, applyingManager string) (*typed.TypedValue, error) { var err error managedAtVersion := map[fieldpath.APIVersion]*fieldpath.Set{} for _, managerSet := range managedFields { - if managerSet.Applied() { - if _, ok := managedAtVersion[managerSet.APIVersion()]; !ok { - managedAtVersion[managerSet.APIVersion()] = fieldpath.NewSet() - } - managedAtVersion[managerSet.APIVersion()] = managedAtVersion[managerSet.APIVersion()].Union(managerSet.Set()) + if _, ok := managedAtVersion[managerSet.APIVersion()]; !ok { + managedAtVersion[managerSet.APIVersion()] = fieldpath.NewSet() } + managedAtVersion[managerSet.APIVersion()] = managedAtVersion[managerSet.APIVersion()].Union(managerSet.Set()) } for version, managed := range managedAtVersion { merged, err = s.Converter.Convert(merged, version) From 5b08b433f39792c88725fc12c38774bf3e5cd169 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Wed, 1 Jul 2020 11:31:08 -0700 Subject: [PATCH 5/5] Add and fix tests to match improved update merging --- merge/multiple_appliers_test.go | 68 ++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/merge/multiple_appliers_test.go b/merge/multiple_appliers_test.go index 4b0f88b5..fcd1e8a9 100644 --- a/merge/multiple_appliers_test.go +++ b/merge/multiple_appliers_test.go @@ -1304,8 +1304,19 @@ func TestMultipleAppliersRealConversion(t *testing.T) { ), }, }, - "applier_updater_shared_ownership_real_conversion": { + "appliers_remove_from_controller_real_conversion": { + // Ensures that an applier can delete associative map items it created after a controller + // modifies them. Ops: []Operation{ + Apply{ + Manager: "apply", + Object: ` + mapOfMapsRecursive: + aaa: + bbb: + `, + APIVersion: "v3", + }, Update{ Manager: "controller", Object: ` @@ -1337,6 +1348,59 @@ func TestMultipleAppliersRealConversion(t *testing.T) { APIVersion: "v3", }, }, + Object: ` + mapOfMapsRecursive: + aaa: + ccc: + `, + APIVersion: "v3", + Managed: fieldpath.ManagedFields{ + "apply": fieldpath.NewVersionedSet( + _NS( + _P("mapOfMapsRecursive", "aaa"), + _P("mapOfMapsRecursive", "ccc"), + ), + "v3", + false, + ), + }, + }, + "applier_updater_shared_ownership_real_conversion": { + // Ensures that when an updater creates maps that they are not deleted when + // an applier shares ownership in them and then later removes them from its applied + // configuration + Ops: []Operation{ + Update{ + Manager: "updater", + Object: ` + mapOfMapsRecursive: + a: + b: + c: + `, + APIVersion: "v1", + }, + Apply{ + Manager: "apply", + Object: ` + mapOfMapsRecursive: + aa: + bb: + cc: + dd: + `, + APIVersion: "v2", + }, + Apply{ + Manager: "apply", + Object: ` + mapOfMapsRecursive: + aaa: + ccc: + `, + APIVersion: "v3", + }, + }, Object: ` mapOfMapsRecursive: aaa: @@ -1346,7 +1410,7 @@ func TestMultipleAppliersRealConversion(t *testing.T) { `, APIVersion: "v3", Managed: fieldpath.ManagedFields{ - "controller": fieldpath.NewVersionedSet( + "updater": fieldpath.NewVersionedSet( _NS( _P("mapOfMapsRecursive"), _P("mapOfMapsRecursive", "a"),