@@ -27,6 +27,7 @@ import (
2727 . "sigs.k8s.io/structured-merge-diff/v3/internal/fixture"
2828 "sigs.k8s.io/structured-merge-diff/v3/merge"
2929 "sigs.k8s.io/structured-merge-diff/v3/typed"
30+ "sigs.k8s.io/structured-merge-diff/v3/value"
3031)
3132
3233func TestMultipleAppliersSet (t * testing.T ) {
@@ -243,6 +244,275 @@ func TestMultipleAppliersSet(t *testing.T) {
243244 }
244245}
245246
247+ var structParser = func () Parser {
248+ parser , err := typed .NewParser (`types:
249+ - name: v1
250+ map:
251+ fields:
252+ - name: struct
253+ type:
254+ namedType: struct
255+ - name: version
256+ type:
257+ scalar: string
258+ - name: struct
259+ map:
260+ fields:
261+ - name: name
262+ type:
263+ scalar: string
264+ - name: scalarField_v1
265+ type:
266+ scalar: string
267+ - name: complexField_v1
268+ type:
269+ namedType: complex
270+ - name: complex
271+ map:
272+ fields:
273+ - name: name
274+ type:
275+ scalar: string
276+ - name: v2
277+ map:
278+ fields:
279+ - name: struct
280+ type:
281+ namedType: struct_v2
282+ - name: version
283+ type:
284+ scalar: string
285+ - name: struct_v2
286+ map:
287+ fields:
288+ - name: name
289+ type:
290+ scalar: string
291+ - name: scalarField_v2
292+ type:
293+ scalar: string
294+ - name: complexField_v2
295+ type:
296+ namedType: complex_v2
297+ - name: complex_v2
298+ map:
299+ fields:
300+ - name: name
301+ type:
302+ scalar: string
303+ ` )
304+ if err != nil {
305+ panic (err )
306+ }
307+ return parser
308+ }()
309+
310+ type versionPair struct {
311+ a fieldpath.APIVersion
312+ b fieldpath.APIVersion
313+ }
314+
315+ func TestMultipleAppliersFieldUnsetting (t * testing.T ) {
316+ for _ , a := range []fieldpath.APIVersion {"v1" , "v2" } {
317+ for _ , b := range []fieldpath.APIVersion {"v1" , "v2" } {
318+ t .Run (fmt .Sprintf ("%s-%s" , a , b ), func (t * testing.T ) {
319+ testMultipleAppliersFieldUnsetting (t , versionPair {a , b })
320+ })
321+ }
322+ }
323+ }
324+
325+ func testMultipleAppliersFieldUnsetting (t * testing.T , versions versionPair ) {
326+ tests := map [string ]TestCase {
327+ "unset_scalar_sole_owner" : {
328+ Ops : []Operation {
329+ Apply {
330+ Manager : "apply-one" ,
331+ APIVersion : versions .a ,
332+ Object : typed .YAMLObject (fmt .Sprintf (`
333+ struct:
334+ name: a
335+ scalarField_%s: a
336+ ` , versions .a )),
337+ },
338+ Apply {
339+ Manager : "apply-one" ,
340+ APIVersion : versions .b ,
341+ Object : `
342+ struct:
343+ name: a
344+ ` ,
345+ },
346+ },
347+ Object : `
348+ struct:
349+ name: a
350+ ` ,
351+ APIVersion : versions .a ,
352+ Managed : fieldpath.ManagedFields {
353+ "apply-one" : fieldpath .NewVersionedSet (
354+ _NS (
355+ _P ("struct" , "name" ),
356+ ),
357+ versions .b ,
358+ false ,
359+ ),
360+ },
361+ },
362+ "unset_scalar_shared_owner" : {
363+ Ops : []Operation {
364+ Apply {
365+ Manager : "apply-one" ,
366+ APIVersion : versions .a ,
367+ Object : typed .YAMLObject (fmt .Sprintf (`
368+ struct:
369+ name: a
370+ scalarField_%s: a
371+ ` , versions .a )),
372+ },
373+ Apply {
374+ Manager : "apply-two" ,
375+ APIVersion : versions .b ,
376+ Object : typed .YAMLObject (fmt .Sprintf (`
377+ struct:
378+ scalarField_%s: a
379+ ` , versions .b )),
380+ },
381+ Apply {
382+ Manager : "apply-one" ,
383+ APIVersion : versions .a ,
384+ Object : `
385+ struct:
386+ name: a
387+ ` ,
388+ },
389+ },
390+ Object : typed .YAMLObject (fmt .Sprintf (`
391+ struct:
392+ name: a
393+ scalarField_%s: a
394+ ` , versions .a )),
395+ APIVersion : versions .a ,
396+ Managed : fieldpath.ManagedFields {
397+ "apply-one" : fieldpath .NewVersionedSet (
398+ _NS (
399+ _P ("struct" , "name" ),
400+ ),
401+ versions .a ,
402+ true ,
403+ ),
404+ "apply-two" : fieldpath .NewVersionedSet (
405+ _NS (
406+ _P ("struct" , fmt .Sprintf ("scalarField_%s" , versions .b )),
407+ ),
408+ versions .b ,
409+ false ,
410+ ),
411+ },
412+ },
413+ "unset_complex_sole_owner" : {
414+ Ops : []Operation {
415+ Apply {
416+ Manager : "apply-one" ,
417+ APIVersion : versions .a ,
418+ Object : typed .YAMLObject (fmt .Sprintf (`
419+ struct:
420+ name: a
421+ complexField_%s:
422+ name: b
423+ ` , versions .a )),
424+ },
425+ Apply {
426+ Manager : "apply-one" ,
427+ APIVersion : versions .b ,
428+ Object : `
429+ struct:
430+ name: a
431+ ` ,
432+ },
433+ },
434+ Object : typed .YAMLObject (fmt .Sprintf (`
435+ struct:
436+ name: a
437+ complexField_%s: null
438+ ` , versions .a )),
439+ APIVersion : versions .a ,
440+ Managed : fieldpath.ManagedFields {
441+ "apply-one" : fieldpath .NewVersionedSet (
442+ _NS (
443+ _P ("struct" , "name" ),
444+ ),
445+ versions .b ,
446+ false ,
447+ ),
448+ },
449+ },
450+ "unset_complex_shared_owner" : {
451+ Ops : []Operation {
452+ Apply {
453+ Manager : "apply-one" ,
454+ APIVersion : versions .a ,
455+ Object : typed .YAMLObject (fmt .Sprintf (`
456+ struct:
457+ name: a
458+ complexField_%s:
459+ name: b
460+ ` , versions .a )),
461+ },
462+ Apply {
463+ Manager : "apply-two" ,
464+ APIVersion : versions .b ,
465+ Object : typed .YAMLObject (fmt .Sprintf (`
466+ struct:
467+ complexField_%s:
468+ name: b
469+ ` , versions .b )),
470+ },
471+ Apply {
472+ Manager : "apply-one" ,
473+ APIVersion : versions .a ,
474+ Object : `
475+ struct:
476+ name: a
477+ ` ,
478+ },
479+ },
480+ Object : typed .YAMLObject (fmt .Sprintf (`
481+ struct:
482+ name: a
483+ complexField_%s:
484+ name: b
485+ ` , versions .a )),
486+ APIVersion : versions .a ,
487+ Managed : fieldpath.ManagedFields {
488+ "apply-one" : fieldpath .NewVersionedSet (
489+ _NS (
490+ _P ("struct" , "name" ),
491+ ),
492+ versions .a ,
493+ false ,
494+ ),
495+ "apply-two" : fieldpath .NewVersionedSet (
496+ _NS (
497+ _P ("struct" , fmt .Sprintf ("complexField_%s" , versions .b ), "name" ),
498+ ),
499+ versions .b ,
500+ false ,
501+ ),
502+ },
503+ },
504+ }
505+
506+ converter := renamingConverter {structParser }
507+ for name , test := range tests {
508+ t .Run (name , func (t * testing.T ) {
509+ if err := test .TestWithConverter (structParser , converter ); err != nil {
510+ t .Fatal (err )
511+ }
512+ })
513+ }
514+ }
515+
246516func TestMultipleAppliersNestedType (t * testing.T ) {
247517 tests := map [string ]TestCase {
248518 "remove_one_keep_one_with_two_sub_items" : {
@@ -1041,6 +1311,56 @@ func (r repeatingConverter) IsMissingVersionError(err error) bool {
10411311 return err == missingVersionError
10421312}
10431313
1314+ // renamingConverter renames fields by substituting the version suffix of the field name. E.g.
1315+ // converting a map with a field named "name_v1" from v1 to v2 renames the field to "name_v2".
1316+ // Fields without a version suffix are not converted; they are the same in all versions.
1317+ // When parsing, this converter will look for the type by using the APIVersion of the
1318+ // object it's trying to parse. If trying to parse a "v1" object, a corresponding "v1" type
1319+ // should exist in the schema of the provided parser.
1320+ type renamingConverter struct {
1321+ parser Parser
1322+ }
1323+
1324+ // Convert implements merge.Converter
1325+ func (r renamingConverter ) Convert (v * typed.TypedValue , version fieldpath.APIVersion ) (* typed.TypedValue , error ) {
1326+ if ! v .AsValue ().IsMap () {
1327+ return v , nil
1328+ }
1329+ inVersion := fieldpath .APIVersion (* v .TypeRef ().NamedType )
1330+ outType := r .parser .Type (string (version ))
1331+ return outType .FromUnstructured (renameConvert (v .AsValue (), string (inVersion ), string (version )))
1332+ }
1333+
1334+ func renameConvert (v value.Value , oldSuffix , newSuffix string ) interface {} {
1335+ if v .IsMap () {
1336+ out := map [string ]interface {}{}
1337+ v .AsMap ().Iterate (func (key string , value value.Value ) bool {
1338+ if strings .HasSuffix (key , oldSuffix ) {
1339+ out [strings .TrimSuffix (key , oldSuffix )+ newSuffix ] = renameConvert (value , oldSuffix , newSuffix )
1340+ } else {
1341+ out [key ] = renameConvert (value , oldSuffix , newSuffix )
1342+ }
1343+ return true
1344+ })
1345+ return out
1346+ }
1347+ if v .IsList () {
1348+ var out []interface {}
1349+ ri := v .AsList ().Range ()
1350+ for ri .Next () {
1351+ _ , v := ri .Item ()
1352+ out = append (out , renameConvert (v , oldSuffix , newSuffix ))
1353+ }
1354+ return out
1355+ }
1356+ return v .Unstructured ()
1357+ }
1358+
1359+ // Convert implements merge.Converter
1360+ func (r renamingConverter ) IsMissingVersionError (err error ) bool {
1361+ return err == missingVersionError
1362+ }
1363+
10441364func BenchmarkMultipleApplierRecursiveRealConversion (b * testing.B ) {
10451365 test := TestCase {
10461366 Ops : []Operation {
0 commit comments