Change:
I created an object with , and later I updated the same object with another . After that I looked at the object's managedFields, and saw nothing about the update. Here is what I got after the second apply: I expected managedFields to get updated by the second . No response <details</details<detailsnone </details<details</details<detailskubeadm </details<detailsdocker </details<details</details
/cc
I also saw the timestamp not updated when I updated using client-go and typed client stubs, Apply method, patch type .
/assign /triage accepted
/cc
The is not updated because you are not changing the structure of the object (adding or removing fields). According to , the time is updated if and only if the fieldSet is changed. It does not take into account the value of fields. One option would be to not compare the fieldSets but directly compare the liveObject & newObject for differences and update the field accordingly. Is it the desired behavior to update the time of when only the value is updated?
Yes! Look at the first paragraph of .
is that documentation correct? Is this what we intended?
I don't see any mention to the timestamp in that first paragraph. We've clarified recently the exact semantic of the timestamp, as defined here: I think you've identified the wrong code if you want to go ahead and fix it, go ahead, I'll happily review!

Discussion:
// Update implements Manager. func (f *managedFieldsUpdater) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) { self := "current-operation" <del> formerSet := managed.Fields()[manager] </del> object, managed, err := f.fieldManager.Update(liveObj, newObj, managed, self) if err != nil { return object, managed, err

} else { managed.Fields()[manager] = vs } <del> // Update the time only if the manager's fieldSet has changed. if formerSet == nil || !managed.Fields()[manager].Set().Equals(formerSet.Set()) { managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()} } </del> <ins> managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()} </ins> } return object, managed, nil

// Apply implements Manager. func (f *managedFieldsUpdater) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) { <del> formerManaged := managed.Fields().Copy() </del> object, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force) if err != nil { return object, managed, err } <del> if object != nil || !managed.Fields().Equals(formerManaged) { </del> <ins> if object != nil { </ins> managed.Times()[fieldManager] = &metav1.Time{Time: time.Now().UTC()} <del> } if object == nil { </del> <ins> } else { </ins> object = liveObj.DeepCopyObject() internal.RemoveObjectManagedFields(object) }

package fieldmanager import ( <ins> "fmt" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" </ins> "reflect" "testing" "time"

"sigs.k8s.io/yaml" ) <del> func TestNoManagedFieldsUpdateDoesntUpdateTime(t *testing.T) { f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "Pod"), "", nil) </del> <ins> func TestManagedFieldsUpdateDoesModifyTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) </ins> <del> obj := &unstructured.Unstructured{Object: map[string]interface{}{}} if err := yaml.Unmarshal([]byte(`{ </del> <ins> err = updateObject(&f, "fieldmanager_test", []byte(`{ </ins> "apiVersion": "v1", <del> "kind": "Pod", </del> <ins> "kind": "ConfigMap", </ins> "metadata": { <del> "name": "pod", "labels": {"app": "nginx"} </del> <ins> "name": "configmap" </ins> }, <del> }`), &obj.Object); err != nil { t.Fatalf("error decoding YAML: %v", err) </del> <ins> "data": { "key": "value" } }`)) if err != nil { t.Fatal(err) </ins> } <ins> previousManagedFields := f.ManagedFields() time.Sleep(time.Second) </ins> <del> if err := f.Update(obj, "fieldmanager_test"); err != nil { t.Fatalf("failed to update object: %v", err) </del> <ins> err = updateObject(&f, "fieldmanager_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key": "new-value" } }`)) if err != nil { t.Fatal(err) </ins> } <del> managed := f.ManagedFields() obj2 := &unstructured.Unstructured{Object: map[string]interface{}{}} if err := yaml.Unmarshal([]byte(`{ </del> <ins> newManagedFields := f.ManagedFields() if previousManagedFields[0].Time.Equal(newManagedFields[0].Time) { t.Errorf("ManagedFields time has not been updated:n%v", newManagedFields) } } func TestManagedFieldsApplyDoesModifyTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) err = applyObject(&f, "fieldmanager_test", []byte(`{ </ins> "apiVersion": "v1", <del> "kind": "Pod", </del> <ins> "kind": "ConfigMap", </ins> "metadata": { <del> "name": "pod", "labels": {"app": "nginx2"} </del> <ins> "name": "configmap" </ins> }, <del> }`), &obj2.Object); err != nil { t.Fatalf("error decoding YAML: %v", err) </del> <ins> "data": { "key": "value" } }`)) if err != nil { t.Fatal(err) } previousManagedFields := f.ManagedFields() time.Sleep(time.Second) err = applyObject(&f, "fieldmanager_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key": "new-value" } }`)) if err != nil { t.Fatal(err) } newManagedFields := f.ManagedFields() if previousManagedFields[0].Time.Equal(newManagedFields[0].Time) { t.Errorf("ManagedFields time has not been updated:n%v", newManagedFields) } } func TestManagedFieldsUpdateWithoutChangesDoesNotModifyTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) err = updateObject(&f, "fieldmanager_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key": "value" } }`)) if err != nil { t.Fatal(err) } previousManagedFields := f.ManagedFields() time.Sleep(time.Second) err = updateObject(&f, "fieldmanager_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key": "value" } }`)) if err != nil { t.Fatal(err) } newManagedFields := f.ManagedFields() if !previousManagedFields[0].Time.Equal(newManagedFields[0].Time) { t.Errorf("ManagedFields time has changed:nBefore:n%vnAfter:n%v", previousManagedFields, newManagedFields) } } func TestManagedFieldsApplyWithoutChangesDoesNotModifyTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) err = applyObject(&f, "fieldmanager_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key": "value" } }`)) if err != nil { t.Fatal(err) } previousManagedFields := f.ManagedFields() time.Sleep(time.Second) err = applyObject(&f, "fieldmanager_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key": "value" } }`)) if err != nil { t.Fatal(err) } newManagedFields := f.ManagedFields() if !previousManagedFields[0].Time.Equal(newManagedFields[0].Time) { t.Errorf("ManagedFields time has changed:nBefore:n%vnAfter:n%v", previousManagedFields, newManagedFields) } } func TestNonManagedFieldsUpdateDoesNotModifyTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) err = updateObject(&f, "fieldmanager_a_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_a": "value" } }`)) if err != nil { t.Fatal(err) } err = updateObject(&f, "fieldmanager_b_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_b": "value" } }`)) if err != nil { t.Fatal(err) </ins> } <ins> previousManagedFields := f.ManagedFields() previousEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range previousManagedFields { previousEntries[entry.Manager] = entry } time.Sleep(time.Second) err = updateObject(&f, "fieldmanager_a_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_a": "value", "key_b": "new-value" } }`)) if err != nil { t.Fatal(err) } newManagedFields := f.ManagedFields() newEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range newManagedFields { newEntries[entry.Manager] = entry } if _, ok := newEntries["fieldmanager_b_test"]; ok { t.Errorf("FieldManager B ManagedFields has changed:n%v", newEntries["fieldmanager_b_test"]) } } func TestNonManagedFieldsApplyDoesNotModifyTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) err = applyObject(&f, "fieldmanager_a_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_a": "value" } }`)) if err != nil { t.Fatal(err) } err = applyObject(&f, "fieldmanager_b_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_b": "value" } }`)) if err != nil { t.Fatal(err) } previousManagedFields := f.ManagedFields() previousEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range previousManagedFields { previousEntries[entry.Manager] = entry } time.Sleep(time.Second) err = applyObject(&f, "fieldmanager_a_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_a": "new-value" } }`)) if err != nil { t.Fatal(err) } newManagedFields := f.ManagedFields() newEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range newManagedFields { newEntries[entry.Manager] = entry } if !previousEntries["fieldmanager_b_test"].Time.Equal(newEntries["fieldmanager_b_test"].Time) { t.Errorf("FieldManager B ManagedFields time changed:nBefore:n%vnAfter:n%v", previousEntries["fieldmanager_b_test"], newEntries["fieldmanager_b_test"]) } } func TestTakingOverManagedFieldsDuringUpdateDoesNotModifyPreviousManagerTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) err = updateObject(&f, "fieldmanager_a_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_a": "value", "key_b": value" } }`)) if err != nil { t.Fatal(err) } previousManagedFields := f.ManagedFields() previousEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range previousManagedFields { previousEntries[entry.Manager] = entry } time.Sleep(time.Second) err = updateObject(&f, "fieldmanager_b_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_b": "new-value" } }`)) if err != nil { t.Fatal(err) } newManagedFields := f.ManagedFields() newEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range newManagedFields { newEntries[entry.Manager] = entry } if !previousEntries["fieldmanager_a_test"].Time.Equal(newEntries["fieldmanager_a_test"].Time) { t.Errorf("FieldManager A ManagedFields time has been updated:nBefore:n%vnAfter:n%v", previousEntries["fieldmanager_a_test"], newEntries["fieldmanager_a_test"]) } } func TestTakingOverManagedFieldsDuringApplyDoesNotModifyPreviousManagerTime(t *testing.T) { var err error f := NewTestFieldManager(schema.FromAPIVersionAndKind("v1", "ConfigMap"), "", nil) err = applyObject(&f, "fieldmanager_a_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_a": "value", "key_b": value" } }`)) if err != nil { t.Fatal(err) } previousManagedFields := f.ManagedFields() previousEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range previousManagedFields { previousEntries[entry.Manager] = entry } </ins> time.Sleep(time.Second) <del> if err := f.Update(obj2, "fieldmanager_test"); err != nil { t.Fatalf("failed to update object: %v", err) </del> <ins> err = applyObject(&f, "fieldmanager_b_test", []byte(`{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "configmap" }, "data": { "key_b": "new-value" } }`)) if err != nil { t.Fatal(err) </ins> } <del> if !reflect.DeepEqual(managed, f.ManagedFields()) { t.Errorf("ManagedFields changed:nBefore:n%vnAfter:n%v", managed, f.ManagedFields()) </del> <ins> newManagedFields := f.ManagedFields() newEntries := map[string]v1.ManagedFieldsEntry{} for _, entry := range newManagedFields { newEntries[entry.Manager] = entry } if !previousEntries["fieldmanager_a_test"].Time.Equal(newEntries["fieldmanager_a_test"].Time) { t.Errorf("FieldManager A ManagedFields time has been updated:nBefore:n%vnAfter:n%v", previousEntries["fieldmanager_a_test"], newEntries["fieldmanager_a_test"]) </ins> } }

return nil, nil, nil } <ins> func updateObject(f *TestFieldManager, fieldManagerName string, object []byte) error { obj := &unstructured.Unstructured{Object: map[string]interface{}{}} if err := yaml.Unmarshal(object, &obj.Object); err != nil { return fmt.Errorf("error decoding YAML: %v", err) } if err := f.Update(obj, fieldManagerName); err != nil { return fmt.Errorf("failed to update object: %v", err) } return nil } func applyObject(f *TestFieldManager, fieldManagerName string, object []byte) error { obj := &unstructured.Unstructured{Object: map[string]interface{}{}} if err := yaml.Unmarshal(object, &obj.Object); err != nil { return fmt.Errorf("error decoding YAML: %v", err) } if err := f.Apply(obj, fieldManagerName, true); err != nil { return fmt.Errorf("failed to apply object: %v", err) } return nil } </ins> // Ensures that if ManagedFieldsUpdater gets a nil value from its nested manager // chain (meaning the operation was a no-op), then the ManagedFieldsUpdater // itself will return a copy of the input live object, with its managed fields