Skip to content

MergeFrom can return invalid patch data when using int64 properties #2603

Closed
@jfremy

Description

@jfremy

I don’t believe this is a known issue (at least, did not find mention of it) so I wanted to raise it.
This bug impacts uses of client.MergeFrom but also helper function like CreateOrPatch (which uses MergeFrom).

Summary: If you have an object (K8s or CRD) that has int64 properties, the MergeFrom call will very likely generate invalid patch data if the values considered cannot be exactly encoded in a float64.

The reason is that the jsonpatch library used to generate the patch is unaware of the underlying type and therefore uses “standard” json marshaling/unmarshaling. So when unmarshaling numbers (int/float), they are stored as float64.
If the number cannot be exactly encoded into a float64, we start seeing incorrect behaviors

The current flow to generate a patch is

  • [controller-runtime] generate JSON []byte representation of both objects - marshaling is correctly based on type
  • [jsonpatch] unmarshal each JSON []byte to map[string]interface{} - unmarshaling can yield loss in precision on numbers / truncated int
  • [jsonpatch] process the values and evaluate whether there is a change - decision is based on invalid values
  • [jsonpatch] marshal patch JSON - marshaling will store value with precision loss

There are two kinds of impact:

  • store the wrong value in an object (you end up storing the value with loss of precision)
  • do no detect a change between the two objects and omit the change in the patch.

I don’t consider it a bug in jsonpatch. The library is following the JSON standard

Looking at the issue, I think that fixing it would require to switch to another json patch generation implementation that is aware of the underlying data types but that’s probably a big change.

I’m including a simple test program to illustrate the issue - MergeFrom generates an empty patch despite the change (I’ve used a Job because it was an existing object with an int64 property).

package main

import (
	v1 "k8s.io/api/batch/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"slices"
	"testing"
)

func TestPatch(t *testing.T) {
	var largeInt64 int64 = 9223372036854775807
	var similarLargeInt64 int64 = 9223372036854775800
	var differentEnoughInt64 int64 = 8223372036854775807
	j := v1.Job{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: "test",
			Name:      "test",
		},
		Spec: v1.JobSpec{
			ActiveDeadlineSeconds: &largeInt64,
		},
	}
	patch := client.MergeFrom(j.DeepCopy())

	j.Spec.ActiveDeadlineSeconds = &differentEnoughInt64
	data, err := patch.Data(&j)
	if err != nil {
		t.Fatalf("patch returned an error: %s", err)
	}
	if slices.Equal(data, []byte("{}")) {
		t.Fatalf("patch returned an empty patch: %s", string(data))
	}

	j.Spec.ActiveDeadlineSeconds = &similarLargeInt64

	data, err = patch.Data(&j)
	if err != nil {
		t.Fatalf("patch returned an error: %s", err)
	}
	if slices.Equal(data, []byte("{}")) {
                // It will fail here because the two int64 values are too close and get truncated to the same value
		t.Fatalf("patch returned an empty patch: %s", string(data))
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugCategorizes issue or PR as related to a bug.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions