diff --git a/api/v1alpha1/function_lifecycle.go b/api/v1alpha1/function_lifecycle.go index ce5ce89..5f37f4f 100644 --- a/api/v1alpha1/function_lifecycle.go +++ b/api/v1alpha1/function_lifecycle.go @@ -2,6 +2,8 @@ package v1alpha1 import ( "fmt" + "sort" + "time" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -224,11 +226,28 @@ func (f *Function) MarkServiceNotReady(reason, messageFormat string, messageA .. const MaxHistoryEntries = 20 -func (f *Function) RecordHistoryEvent(message string) { - f.Status.History = append([]FunctionStatusHistoryEntry{{ +type historyEventOption func(entry *FunctionStatusHistoryEntry) + +func WithHistoryEventTime(t time.Time) historyEventOption { + return func(entry *FunctionStatusHistoryEntry) { + entry.Time = metav1.NewTime(t) + } +} + +func (f *Function) RecordHistoryEvent(message string, options ...historyEventOption) { + entry := FunctionStatusHistoryEntry{ Time: metav1.Now(), Message: message, - }}, f.Status.History...) + } + + for _, opt := range options { + opt(&entry) + } + + f.Status.History = append(f.Status.History, entry) + sort.Slice(f.Status.History, func(i, j int) bool { + return f.Status.History[i].Time.After(f.Status.History[j].Time.Time) + }) if len(f.Status.History) > MaxHistoryEntries { f.Status.History = f.Status.History[:MaxHistoryEntries] } diff --git a/api/v1alpha1/function_lifecycle_test.go b/api/v1alpha1/function_lifecycle_test.go index cb34964..57cca1a 100644 --- a/api/v1alpha1/function_lifecycle_test.go +++ b/api/v1alpha1/function_lifecycle_test.go @@ -3,6 +3,7 @@ package v1alpha1 import ( "fmt" "testing" + "time" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,7 +29,7 @@ func TestRecordHistoryEvent(t *testing.T) { { name: "prepends event to existing history", existingHistory: []FunctionStatusHistoryEntry{ - {Time: metav1.Now(), Message: "Older event"}, + {Time: metav1.NewTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), Message: "Older event"}, }, newMessage: "Newer event", expectedLen: 2, @@ -38,10 +39,11 @@ func TestRecordHistoryEvent(t *testing.T) { { name: "trims oldest entries when exceeding max", existingHistory: func() []FunctionStatusHistoryEntry { + base := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) entries := make([]FunctionStatusHistoryEntry, MaxHistoryEntries) for i := range entries { entries[i] = FunctionStatusHistoryEntry{ - Time: metav1.Now(), + Time: metav1.NewTime(base.Add(time.Duration(i) * time.Minute)), Message: fmt.Sprintf("Event %d", i), } } @@ -50,7 +52,7 @@ func TestRecordHistoryEvent(t *testing.T) { newMessage: "Overflow event", expectedLen: MaxHistoryEntries, expectedFirst: "Overflow event", - expectedLast: fmt.Sprintf("Event %d", MaxHistoryEntries-2), + expectedLast: fmt.Sprintf("Event %d", 1), }, } @@ -76,9 +78,10 @@ func TestRecordHistoryEvent(t *testing.T) { func TestRecordHistoryEventFIFOOrder(t *testing.T) { f := &Function{} + base := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) for i := 0; i < MaxHistoryEntries+5; i++ { - f.RecordHistoryEvent(fmt.Sprintf("Event %d", i)) + f.RecordHistoryEvent(fmt.Sprintf("Event %d", i), WithHistoryEventTime(base.Add(time.Duration(i)*time.Minute))) } if len(f.Status.History) != MaxHistoryEntries { @@ -93,6 +96,28 @@ func TestRecordHistoryEventFIFOOrder(t *testing.T) { } } +func TestRecordHistoryEventSortsByTime(t *testing.T) { + f := &Function{} + now := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC) + + f.RecordHistoryEvent("Second event", WithHistoryEventTime(now)) + f.RecordHistoryEvent("First event (older)", WithHistoryEventTime(now.Add(-1*time.Hour))) + f.RecordHistoryEvent("Third event", WithHistoryEventTime(now.Add(1*time.Hour))) + + if len(f.Status.History) != 3 { + t.Fatalf("expected 3 entries, got %d", len(f.Status.History)) + } + if f.Status.History[0].Message != "Third event" { + t.Errorf("expected first entry to be newest, got %q", f.Status.History[0].Message) + } + if f.Status.History[1].Message != "Second event" { + t.Errorf("expected second entry to be middle, got %q", f.Status.History[1].Message) + } + if f.Status.History[2].Message != "First event (older)" { + t.Errorf("expected last entry to be oldest, got %q", f.Status.History[2].Message) + } +} + func TestRecordHistoryEventSetsTime(t *testing.T) { f := &Function{} f.RecordHistoryEvent("test event") diff --git a/internal/controller/function_controller.go b/internal/controller/function_controller.go index 6fbf30c..000702d 100644 --- a/internal/controller/function_controller.go +++ b/internal/controller/function_controller.go @@ -154,6 +154,7 @@ func applyLastDeployedAnnotation(ctx context.Context, function *v1alpha1.Functio log.FromContext(ctx).Info("could not parse "+funcAnnotationLastDeployed+" annotation", "error", err) } else { function.Status.Deployment.ImageBuilt = metav1.NewTime(t) + function.RecordHistoryEvent("Function was deployed/redeployed", v1alpha1.WithHistoryEventTime(t)) } } } diff --git a/internal/controller/function_controller_test.go b/internal/controller/function_controller_test.go index 2431322..0d7c434 100644 --- a/internal/controller/function_controller_test.go +++ b/internal/controller/function_controller_test.go @@ -476,6 +476,16 @@ var _ = Describe("Function Controller", func() { expectedTime, err := time.Parse(time.RFC3339, "2026-01-02T15:04:05+06:00") Expect(err).NotTo(HaveOccurred()) Expect(status.Deployment.ImageBuilt.UTC()).To(Equal(expectedTime.UTC())) + + // check if it is in the history too + Expect(status.History).To(ContainElement( + SatisfyAll( + HaveField("Message", "Function was deployed/redeployed"), + WithTransform(func(e functionsdevv1alpha1.FunctionStatusHistoryEntry) time.Time { + return e.Time.UTC() + }, Equal(expectedTime.UTC())), + ), + )) }, }), Entry("should set ServiceReady condition to false with unknown reason when ready status is empty", reconcileTestCase{