From f9f8c43e8a7ae3db305f31cb38e8d51485c90178 Mon Sep 17 00:00:00 2001
From: Vladislav Kuzmin <vkuzmin@mirantis.com>
Date: Thu, 15 Oct 2020 16:49:01 +0400
Subject: [PATCH] Add timestamp for events

Relates-To: #339
Change-Id: Iec939e167b6ae3c72aa31e102407d0fffc370766
---
 pkg/bootstrap/isogen/executor.go       | 51 ++++++++------------
 pkg/bootstrap/isogen/executor_test.go  | 52 +++++++++------------
 pkg/clusterctl/client/executor.go      | 64 +++++++++-----------------
 pkg/clusterctl/client/executor_test.go | 43 ++++++++---------
 pkg/events/events.go                   | 31 +++++++++++++
 5 files changed, 113 insertions(+), 128 deletions(-)

diff --git a/pkg/bootstrap/isogen/executor.go b/pkg/bootstrap/isogen/executor.go
index 9427bd879..0126a30e1 100644
--- a/pkg/bootstrap/isogen/executor.go
+++ b/pkg/bootstrap/isogen/executor.go
@@ -83,22 +83,16 @@ func (c *Executor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
 		return
 	}
 
-	evtCh <- events.Event{
-		Type: events.IsogenType,
-		IsogenEvent: events.IsogenEvent{
-			Operation: events.IsogenStart,
-			Message:   "starting ISO generation",
-		},
-	}
+	evtCh <- events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+		Operation: events.IsogenStart,
+		Message:   "starting ISO generation",
+	})
 
 	if opts.DryRun {
 		log.Print("command isogen will be executed")
-		evtCh <- events.Event{
-			Type: events.IsogenType,
-			IsogenEvent: events.IsogenEvent{
-				Operation: events.IsogenEnd,
-			},
-		}
+		evtCh <- events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+			Operation: events.IsogenEnd,
+		})
 		return
 	}
 
@@ -131,26 +125,20 @@ func (c *Executor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
 		return
 	}
 
-	evtCh <- events.Event{
-		Type: events.IsogenType,
-		IsogenEvent: events.IsogenEvent{
-			Operation: events.IsogenValidation,
-			Message:   "image is generated successfully, verifying artifacts",
-		},
-	}
+	evtCh <- events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+		Operation: events.IsogenValidation,
+		Message:   "image is generated successfully, verifying artifacts",
+	})
 	err = verifyArtifacts(c.imgConf)
 	if err != nil {
 		handleError(evtCh, err)
 		return
 	}
 
-	evtCh <- events.Event{
-		Type: events.IsogenType,
-		IsogenEvent: events.IsogenEvent{
-			Operation: events.IsogenEnd,
-			Message:   "iso generation is complete and artifacts verified",
-		},
-	}
+	evtCh <- events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+		Operation: events.IsogenEnd,
+		Message:   "iso generation is complete and artifacts verified",
+	})
 }
 
 // Validate executor configuration and documents
@@ -166,10 +154,7 @@ func (c *Executor) Render(w io.Writer, _ ifc.RenderOptions) error {
 }
 
 func handleError(ch chan<- events.Event, err error) {
-	ch <- events.Event{
-		Type: events.ErrorType,
-		ErrorEvent: events.ErrorEvent{
-			Error: err,
-		},
-	}
+	ch <- events.NewEvent().WithErrorEvent(events.ErrorEvent{
+		Error: err,
+	})
 }
diff --git a/pkg/bootstrap/isogen/executor_test.go b/pkg/bootstrap/isogen/executor_test.go
index e3a5ad04f..0ba914a6e 100644
--- a/pkg/bootstrap/isogen/executor_test.go
+++ b/pkg/bootstrap/isogen/executor_test.go
@@ -16,6 +16,7 @@ package isogen
 
 import (
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -109,24 +110,15 @@ func TestExecutorRun(t *testing.T) {
 				waitUntilFinished: func() error { return nil },
 			},
 			expectedEvt: []events.Event{
-				{
-					Type: events.IsogenType,
-					IsogenEvent: events.IsogenEvent{
-						Operation: events.IsogenStart,
-					},
-				},
-				{
-					Type: events.IsogenType,
-					IsogenEvent: events.IsogenEvent{
-						Operation: events.IsogenValidation,
-					},
-				},
-				{
-					Type: events.IsogenType,
-					IsogenEvent: events.IsogenEvent{
-						Operation: events.IsogenEnd,
-					},
-				},
+				events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+					Operation: events.IsogenStart,
+				}),
+				events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+					Operation: events.IsogenValidation,
+				}),
+				events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+					Operation: events.IsogenEnd,
+				}),
 			},
 		},
 		{
@@ -140,12 +132,9 @@ func TestExecutorRun(t *testing.T) {
 			},
 
 			expectedEvt: []events.Event{
-				{
-					Type: events.IsogenType,
-					IsogenEvent: events.IsogenEvent{
-						Operation: events.IsogenStart,
-					},
-				},
+				events.NewEvent().WithIsogenEvent(events.IsogenEvent{
+					Operation: events.IsogenStart,
+				}),
 				wrapError(container.ErrRunContainerCommand{Cmd: "super fail"}),
 			},
 		},
@@ -163,24 +152,27 @@ func TestExecutorRun(t *testing.T) {
 			go executor.Run(ch, ifc.RunOptions{})
 			var actualEvt []events.Event
 			for evt := range ch {
+				// Skip timestamp for comparison
+				evt.Timestamp = time.Time{}
 				if evt.Type == events.IsogenType {
 					// Set message to empty string, so it's not compared
 					evt.IsogenEvent.Message = ""
 				}
 				actualEvt = append(actualEvt, evt)
 			}
+			for i := range tt.expectedEvt {
+				// Skip timestamp for comparison
+				tt.expectedEvt[i].Timestamp = time.Time{}
+			}
 			assert.Equal(t, tt.expectedEvt, actualEvt)
 		})
 	}
 }
 
 func wrapError(err error) events.Event {
-	return events.Event{
-		Type: events.ErrorType,
-		ErrorEvent: events.ErrorEvent{
-			Error: err,
-		},
-	}
+	return events.NewEvent().WithErrorEvent(events.ErrorEvent{
+		Error: err,
+	})
 }
 
 func testBundleFactory(path string) document.BundleFactoryFunc {
diff --git a/pkg/clusterctl/client/executor.go b/pkg/clusterctl/client/executor.go
index 0f31d9ce4..fd2564d65 100644
--- a/pkg/clusterctl/client/executor.go
+++ b/pkg/clusterctl/client/executor.go
@@ -84,13 +84,10 @@ func (c *ClusterctlExecutor) Run(evtCh chan events.Event, opts ifc.RunOptions) {
 }
 
 func (c *ClusterctlExecutor) move(opts ifc.RunOptions, evtCh chan events.Event) {
-	evtCh <- events.Event{
-		Type: events.ClusterctlType,
-		ClusterctlEvent: events.ClusterctlEvent{
-			Operation: events.ClusterctlMoveStart,
-			Message:   "starting clusterctl move executor",
-		},
-	}
+	evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+		Operation: events.ClusterctlMoveStart,
+		Message:   "starting clusterctl move executor",
+	})
 	ns := c.options.MoveOptions.Namespace
 	kubeConfigFile, cleanup, err := c.kubecfg.GetFile()
 	if err != nil {
@@ -113,23 +110,17 @@ func (c *ClusterctlExecutor) move(opts ifc.RunOptions, evtCh chan events.Event)
 		}
 	}
 
-	evtCh <- events.Event{
-		Type: events.ClusterctlType,
-		ClusterctlEvent: events.ClusterctlEvent{
-			Operation: events.ClusterctlMoveEnd,
-			Message:   "clusterctl move completed successfully",
-		},
-	}
+	evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+		Operation: events.ClusterctlMoveEnd,
+		Message:   "clusterctl move completed successfully",
+	})
 }
 
 func (c *ClusterctlExecutor) init(opts ifc.RunOptions, evtCh chan events.Event) {
-	evtCh <- events.Event{
-		Type: events.ClusterctlType,
-		ClusterctlEvent: events.ClusterctlEvent{
-			Operation: events.ClusterctlInitStart,
-			Message:   "starting clusterctl init executor",
-		},
-	}
+	evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+		Operation: events.ClusterctlInitStart,
+		Message:   "starting clusterctl init executor",
+	})
 	kubeConfigFile, cleanup, err := c.kubecfg.GetFile()
 	if err != nil {
 		c.handleErr(err, evtCh)
@@ -141,13 +132,10 @@ func (c *ClusterctlExecutor) init(opts ifc.RunOptions, evtCh chan events.Event)
 	if opts.DryRun {
 		// TODO (dukov) add more details to dry-run
 		log.Print("command 'clusterctl init' is going to be executed")
-		evtCh <- events.Event{
-			Type: events.ClusterctlType,
-			ClusterctlEvent: events.ClusterctlEvent{
-				Operation: events.ClusterctlInitEnd,
-				Message:   "clusterctl init dry-run completed successfully",
-			},
-		}
+		evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+			Operation: events.ClusterctlInitEnd,
+			Message:   "clusterctl init dry-run completed successfully",
+		})
 		return
 	}
 	// Use cluster name as context in kubeconfig file
@@ -155,22 +143,16 @@ func (c *ClusterctlExecutor) init(opts ifc.RunOptions, evtCh chan events.Event)
 	if err != nil {
 		c.handleErr(err, evtCh)
 	}
-	evtCh <- events.Event{
-		Type: events.ClusterctlType,
-		ClusterctlEvent: events.ClusterctlEvent{
-			Operation: events.ClusterctlInitEnd,
-			Message:   "clusterctl init completed successfully",
-		},
-	}
+	evtCh <- events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+		Operation: events.ClusterctlInitEnd,
+		Message:   "clusterctl init completed successfully",
+	})
 }
 
 func (c *ClusterctlExecutor) handleErr(err error, evtCh chan events.Event) {
-	evtCh <- events.Event{
-		Type: events.ErrorType,
-		ErrorEvent: events.ErrorEvent{
-			Error: err,
-		},
-	}
+	evtCh <- events.NewEvent().WithErrorEvent(events.ErrorEvent{
+		Error: err,
+	})
 }
 
 // Validate executor configuration and documents
diff --git a/pkg/clusterctl/client/executor_test.go b/pkg/clusterctl/client/executor_test.go
index bef6efe8a..2ff892988 100644
--- a/pkg/clusterctl/client/executor_test.go
+++ b/pkg/clusterctl/client/executor_test.go
@@ -19,6 +19,7 @@ import (
 	"errors"
 	"fmt"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -122,12 +123,9 @@ func TestExecutorRun(t *testing.T) {
 			},
 			bundlePath: "testdata/executor_init",
 			expectedEvt: []events.Event{
-				{
-					Type: events.ClusterctlType,
-					ClusterctlEvent: events.ClusterctlEvent{
-						Operation: events.ClusterctlInitStart,
-					},
-				},
+				events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+					Operation: events.ClusterctlInitStart,
+				}),
 				wrapError(errTmpFile),
 			},
 		},
@@ -146,18 +144,12 @@ func TestExecutorRun(t *testing.T) {
 			},
 			bundlePath: "testdata/executor_init",
 			expectedEvt: []events.Event{
-				{
-					Type: events.ClusterctlType,
-					ClusterctlEvent: events.ClusterctlEvent{
-						Operation: events.ClusterctlInitStart,
-					},
-				},
-				{
-					Type: events.ClusterctlType,
-					ClusterctlEvent: events.ClusterctlEvent{
-						Operation: events.ClusterctlInitEnd,
-					},
-				},
+				events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+					Operation: events.ClusterctlInitStart,
+				}),
+				events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{
+					Operation: events.ClusterctlInitEnd,
+				}),
 			},
 		},
 		// TODO add move tests here
@@ -180,12 +172,18 @@ func TestExecutorRun(t *testing.T) {
 			go executor.Run(ch, ifc.RunOptions{DryRun: true})
 			var actualEvt []events.Event
 			for evt := range ch {
+				// Skip timmestamp for comparison
+				evt.Timestamp = time.Time{}
 				if evt.Type == events.ClusterctlType {
 					// Set message to empty string, so it's not compared
 					evt.ClusterctlEvent.Message = ""
 				}
 				actualEvt = append(actualEvt, evt)
 			}
+			for i := range tt.expectedEvt {
+				// Skip timmestamp for comparison
+				tt.expectedEvt[i].Timestamp = time.Time{}
+			}
 			assert.Equal(t, tt.expectedEvt, actualEvt)
 		})
 	}
@@ -237,10 +235,7 @@ func executorDoc(t *testing.T, action string) document.Document {
 }
 
 func wrapError(err error) events.Event {
-	return events.Event{
-		Type: events.ErrorType,
-		ErrorEvent: events.ErrorEvent{
-			Error: err,
-		},
-	}
+	return events.NewEvent().WithErrorEvent(events.ErrorEvent{
+		Error: err,
+	})
 }
diff --git a/pkg/events/events.go b/pkg/events/events.go
index a5aa821cd..4268fcff0 100644
--- a/pkg/events/events.go
+++ b/pkg/events/events.go
@@ -15,6 +15,8 @@
 package events
 
 import (
+	"time"
+
 	applyevent "sigs.k8s.io/cli-utils/pkg/apply/event"
 	statuspollerevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
 )
@@ -40,6 +42,7 @@ const (
 // Event holds all possible events that can be produced by airship
 type Event struct {
 	Type              Type
+	Timestamp         time.Time
 	ApplierEvent      applyevent.Event
 	ErrorEvent        ErrorEvent
 	StatusPollerEvent statuspollerevent.Event
@@ -47,11 +50,25 @@ type Event struct {
 	IsogenEvent       IsogenEvent
 }
 
+// NewEvent create new event with timestamp
+func NewEvent() Event {
+	return Event{
+		Timestamp: time.Now(),
+	}
+}
+
 // ErrorEvent is produced when error is encountered
 type ErrorEvent struct {
 	Error error
 }
 
+// WithErrorEvent sets type and actual error event
+func (e Event) WithErrorEvent(event ErrorEvent) Event {
+	e.Type = ErrorType
+	e.ErrorEvent = event
+	return e
+}
+
 // ClusterctlOperation type
 type ClusterctlOperation int
 
@@ -72,6 +89,13 @@ type ClusterctlEvent struct {
 	Message   string
 }
 
+// WithClusterctlEvent sets type and actual clusterctl event
+func (e Event) WithClusterctlEvent(concreteEvent ClusterctlEvent) Event {
+	e.Type = ClusterctlType
+	e.ClusterctlEvent = concreteEvent
+	return e
+}
+
 // IsogenOperation type
 type IsogenOperation int
 
@@ -89,3 +113,10 @@ type IsogenEvent struct {
 	Operation IsogenOperation
 	Message   string
 }
+
+// WithIsogenEvent sets type and actual isogen event
+func (e Event) WithIsogenEvent(concreteEvent IsogenEvent) Event {
+	e.Type = IsogenType
+	e.IsogenEvent = concreteEvent
+	return e
+}