diff --git a/pkg/apis/core/validation/events.go b/pkg/apis/core/validation/events.go index eeecac218e5..d2578f229e0 100644 --- a/pkg/apis/core/validation/events.go +++ b/pkg/apis/core/validation/events.go @@ -95,7 +95,18 @@ func ValidateEventUpdate(newEvent, oldEvent *core.Event, requestVersion schema.G allErrs = append(allErrs, ValidateImmutableField(newEvent.Count, oldEvent.Count, field.NewPath("count"))...) allErrs = append(allErrs, ValidateImmutableField(newEvent.Reason, oldEvent.Reason, field.NewPath("reason"))...) allErrs = append(allErrs, ValidateImmutableField(newEvent.Type, oldEvent.Type, field.NewPath("type"))...) - allErrs = append(allErrs, ValidateImmutableField(newEvent.EventTime, oldEvent.EventTime, field.NewPath("eventTime"))...) + + // Disallow changes to eventTime greater than microsecond-level precision. + // Tolerating sub-microsecond changes is required to tolerate updates + // from clients that correctly truncate to microsecond-precision when serializing, + // or from clients built with incorrect nanosecond-precision protobuf serialization. + // See https://github.com/kubernetes/kubernetes/issues/111928 + newTruncated := newEvent.EventTime.Truncate(time.Microsecond).UTC() + oldTruncated := oldEvent.EventTime.Truncate(time.Microsecond).UTC() + if newTruncated != oldTruncated { + allErrs = append(allErrs, ValidateImmutableField(newEvent.EventTime, oldEvent.EventTime, field.NewPath("eventTime"))...) + } + allErrs = append(allErrs, ValidateImmutableField(newEvent.Action, oldEvent.Action, field.NewPath("action"))...) allErrs = append(allErrs, ValidateImmutableField(newEvent.Related, oldEvent.Related, field.NewPath("related"))...) allErrs = append(allErrs, ValidateImmutableField(newEvent.ReportingController, oldEvent.ReportingController, field.NewPath("reportingController"))...) diff --git a/pkg/apis/core/validation/events_test.go b/pkg/apis/core/validation/events_test.go index e9a2f69c316..66427b65b08 100644 --- a/pkg/apis/core/validation/events_test.go +++ b/pkg/apis/core/validation/events_test.go @@ -20,7 +20,7 @@ import ( "testing" "time" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" eventsv1 "k8s.io/api/events/v1" eventsv1beta1 "k8s.io/api/events/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1312,3 +1312,67 @@ func TestValidateEventUpdateForNewV1Events(t *testing.T) { } } } + +func TestEventV1EventTimeImmutability(t *testing.T) { + testcases := []struct { + Name string + Old metav1.MicroTime + New metav1.MicroTime + Valid bool + }{ + { + Name: "noop microsecond precision", + Old: metav1.NewMicroTime(time.Unix(100, int64(5*time.Microsecond))), + New: metav1.NewMicroTime(time.Unix(100, int64(5*time.Microsecond))), + Valid: true, + }, + { + Name: "noop nanosecond precision", + Old: metav1.NewMicroTime(time.Unix(100, int64(5*time.Nanosecond))), + New: metav1.NewMicroTime(time.Unix(100, int64(5*time.Nanosecond))), + Valid: true, + }, + { + Name: "modify nanoseconds within the same microsecond", + Old: metav1.NewMicroTime(time.Unix(100, int64(5*time.Nanosecond))), + New: metav1.NewMicroTime(time.Unix(100, int64(6*time.Nanosecond))), + Valid: true, + }, + { + Name: "modify microseconds", + Old: metav1.NewMicroTime(time.Unix(100, int64(5*time.Microsecond))), + New: metav1.NewMicroTime(time.Unix(100, int64(5*time.Microsecond-time.Nanosecond))), + Valid: false, + }, + { + Name: "modify seconds", + Old: metav1.NewMicroTime(time.Unix(100, 0)), + New: metav1.NewMicroTime(time.Unix(101, 0)), + Valid: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + oldEvent := &core.Event{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: metav1.NamespaceSystem, ResourceVersion: "2"}, + InvolvedObject: core.ObjectReference{APIVersion: "v2", Kind: "Node"}, + Series: &core.EventSeries{Count: 2, LastObservedTime: tc.Old}, + EventTime: tc.Old, + ReportingController: "k8s.io/my-controller", + ReportingInstance: "node-xyz", + Action: "Do", + Reason: "Yeees", + Type: "Normal", + } + + newEvent := oldEvent.DeepCopy() + newEvent.EventTime = tc.New + + updateErrs := ValidateEventUpdate(newEvent, oldEvent, eventsv1.SchemeGroupVersion) + if e, a := tc.Valid, len(updateErrs) == 0; e != a { + t.Errorf("%v: expected valid=%v, got %v: %v", tc.Valid, e, a, updateErrs) + } + }) + } +}