Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions api/v1alpha1/function_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package v1alpha1

import (
"fmt"
"sort"
"time"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -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]
}
Expand Down
33 changes: 29 additions & 4 deletions api/v1alpha1/function_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package v1alpha1
import (
"fmt"
"testing"
"time"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -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,
Expand All @@ -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),
}
}
Expand All @@ -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),
},
}

Expand All @@ -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 {
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions internal/controller/function_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions internal/controller/function_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading