diff --git a/pkg/events/events.go b/pkg/events/events.go index 8ab231d79..3eca9686b 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -15,6 +15,7 @@ package events import ( + "fmt" "time" applyevent "sigs.k8s.io/cli-utils/pkg/apply/event" @@ -56,6 +57,87 @@ type Event struct { GenericContainerEvent GenericContainerEvent } +//GenericEvent generalized type for custom events +type GenericEvent struct { + Type string + Operation string + Message string + Timestamp time.Time +} + +var mapTypeToEvent = map[Type]string{ + ClusterctlType: "ClusterctlEvent", + IsogenType: "IsogenEvent", + BootstrapType: "BootstrapEvent", + GenericContainerType: "GenericContainerEvent", +} + +var unknownEventType = map[Type]string{ + ApplierType: "ApplierType", + ErrorType: "ErrorType", + StatusPollerType: "StatusPollerType", + WaitType: "WaitType", +} + +var isogenOperationToString = map[IsogenOperation]string{ + IsogenStart: "IsogenStart", + IsogenValidation: "IsogenValidation", + IsogenEnd: "IsogenEnd", +} + +var clusterctlOperationToString = map[ClusterctlOperation]string{ + ClusterctlInitStart: "ClusterctlInitStart", + ClusterctlInitEnd: "ClusterctlInitEnd", + ClusterctlMoveStart: "ClusterctlMoveStart", + ClusterctlMoveEnd: "ClusterctlMoveEnd", +} + +var bootstrapOperationToString = map[BootstrapOperation]string{ + BootstrapStart: "BootstrapStart", + BootstrapDryRun: "BootstrapDryRun", + BootstrapValidation: "BootstrapValidation", + BootstrapRun: "BootstrapRun", + BootstrapEnd: "BootstrapEnd", +} + +var genericContainerOperationToString = map[GenericContainerOperation]string{ + GenericContainerStart: "GenericContainerStart", + GenericContainerStop: "GenericContainerStop", +} + +//Normalize cast Event to GenericEvent type +func Normalize(e Event) GenericEvent { + var eventType string + if t, exists := mapTypeToEvent[e.Type]; exists { + eventType = t + } else { + eventType = fmt.Sprintf("Unknown event type: %v", unknownEventType[e.Type]) + } + + var operation, message string + switch e.Type { + case ClusterctlType: + operation = clusterctlOperationToString[e.ClusterctlEvent.Operation] + message = e.ClusterctlEvent.Message + case IsogenType: + operation = isogenOperationToString[e.IsogenEvent.Operation] + message = e.IsogenEvent.Message + case BootstrapType: + operation = bootstrapOperationToString[e.BootstrapEvent.Operation] + message = e.BootstrapEvent.Message + case GenericContainerType: + operation = genericContainerOperationToString[e.GenericContainerEvent.Operation] + message = e.GenericContainerEvent.Message + } + + return GenericEvent{ + Type: eventType, + Operation: operation, + Message: message, + Timestamp: e.Timestamp, + } +} + // NewEvent create new event with timestamp func NewEvent() Event { return Event{ diff --git a/pkg/events/events_test.go b/pkg/events/events_test.go new file mode 100644 index 000000000..f21b543c6 --- /dev/null +++ b/pkg/events/events_test.go @@ -0,0 +1,61 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package events_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "opendev.org/airship/airshipctl/pkg/events" +) + +func TestNormalize(t *testing.T) { + tests := []struct { + name string + sourceEvent events.Event + expectedEvent events.GenericEvent + }{ + { + name: "Unknow event type", + sourceEvent: events.NewEvent().WithErrorEvent(events.ErrorEvent{}), + expectedEvent: events.GenericEvent{ + Type: "Unknown event type: ErrorType", + }, + }, + { + name: "Clusterctl event type", + sourceEvent: events.NewEvent().WithClusterctlEvent(events.ClusterctlEvent{ + Operation: events.ClusterctlInitStart, + Message: "Clusterctl init start", + }), + expectedEvent: events.GenericEvent{ + Type: "ClusterctlEvent", + Message: "Clusterctl init start", + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ge := events.Normalize(tt.sourceEvent) + assert.Equal(t, tt.expectedEvent.Type, ge.Type) + if tt.expectedEvent.Type != "Unknown event type: ErrorType" { + assert.Equal(t, tt.expectedEvent.Message, ge.Message) + } + }) + } +} diff --git a/pkg/events/printers.go b/pkg/events/printers.go new file mode 100644 index 000000000..31fd28920 --- /dev/null +++ b/pkg/events/printers.go @@ -0,0 +1,69 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package events + +import ( + "encoding/json" + "io" + + "sigs.k8s.io/yaml" + + "opendev.org/airship/airshipctl/pkg/log" +) + +const ( + // YAMLPrinter format for event printer output + YAMLPrinter = "yaml" + // JSONPrinter format for event printer output + JSONPrinter = "json" +) + +// NewGenericPrinter returns event printer +func NewGenericPrinter(writer io.Writer, formatterType string) GenericPrinter { + var formatter func(o interface{}) ([]byte, error) + switch formatterType { + case YAMLPrinter: + formatter = yaml.Marshal + case JSONPrinter: + formatter = json.Marshal + default: + log.Fatal("Event printer received wrong type of event formatter") + } + return GenericPrinter{ + formatter: formatter, + writer: writer, + } +} + +// GenericPrinter object represents event printer +type GenericPrinter struct { + formatter func(interface{}) ([]byte, error) + writer io.Writer +} + +// PrintEvent write event details +func (p GenericPrinter) PrintEvent(ge GenericEvent) error { + data, err := p.formatter(map[string]interface{}{ + "Type": ge.Type, + "Operation": ge.Operation, + "Message": ge.Message, + "Timestamp": ge.Timestamp, + }) + if err != nil { + return err + } + _, err = p.writer.Write(data) + return err +} diff --git a/pkg/events/printers_test.go b/pkg/events/printers_test.go new file mode 100644 index 000000000..7db213d9b --- /dev/null +++ b/pkg/events/printers_test.go @@ -0,0 +1,69 @@ +/* + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package events_test + +import ( + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "opendev.org/airship/airshipctl/pkg/events" +) + +type fakeWriter struct { + writeErr error +} + +var _ io.Writer = fakeWriter{} + +func (f fakeWriter) Write(p []byte) (n int, err error) { + return 0, f.writeErr +} + +func TestPrintEvent(t *testing.T) { + tests := []struct { + name string + formatterType string + errString string + writer io.Writer + }{ + { + name: "Fail on formatter type", + formatterType: events.YAMLPrinter, + errString: "Error on write", + writer: fakeWriter{ + writeErr: fmt.Errorf("Error on write"), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + p := events.NewGenericPrinter(tt.writer, tt.formatterType) + e := events.NewEvent().WithIsogenEvent(events.IsogenEvent{ + Operation: events.IsogenStart, + Message: "starting ISO generation", + }) + ge := events.Normalize(e) + err := p.PrintEvent(ge) + if tt.errString != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errString) + } + }) + } +} diff --git a/pkg/events/processor.go b/pkg/events/processor.go index 6fe64675c..a72895152 100644 --- a/pkg/events/processor.go +++ b/pkg/events/processor.go @@ -32,17 +32,21 @@ type EventProcessor interface { // DefaultProcessor is implementation of EventProcessor type DefaultProcessor struct { - errors []error - applierChan chan<- applyevent.Event + errors []error + applierChan chan<- applyevent.Event + genericPrinter GenericPrinter } // NewDefaultProcessor returns instance of DefaultProcessor as interface Implementation func NewDefaultProcessor(streams genericclioptions.IOStreams) EventProcessor { applyCh := make(chan applyevent.Event) go printers.GetPrinter(printers.EventsPrinter, streams).Print(applyCh, common.DryRunNone) + // printer for custom airshipctl events + genericPrinter := NewGenericPrinter(log.Writer(), JSONPrinter) return &DefaultProcessor{ - errors: []error{}, - applierChan: applyCh, + errors: []error{}, + applierChan: applyCh, + genericPrinter: genericPrinter, } } @@ -55,19 +59,16 @@ func (p *DefaultProcessor) Process(ch <-chan Event) error { case ErrorType: log.Printf("Received error on event channel %v", e.ErrorEvent) p.errors = append(p.errors, e.ErrorEvent.Error) - case ClusterctlType, IsogenType, GenericContainerType: - // TODO each event needs to be interface that allows us to print it for example - // Stringer interface or AsYAML for further processing. - // For now we print the event object as is - log.Printf("Received event: %v", e) - case BootstrapType: - log.Printf("%s", e.BootstrapEvent.Message) case StatusPollerType: log.Fatalf("Processing for status poller events are not yet implemented") case WaitType: log.Fatalf("Processing for wait events are not yet implemented") default: - log.Fatalf("Unknown event type received: %d", e.Type) + ge := Normalize(e) + err := p.genericPrinter.PrintEvent(ge) + if err != nil { + p.errors = append(p.errors, err) + } } } return checkErrors(p.errors)