Add printers for events

It will allow users to see what's happening during phase execution.

Change-Id: I31c4f98ccbd811ac88c7eabdf5efbe6ffaae6a41
Relates-To: #339
This commit is contained in:
Vladislav Kuzmin 2020-10-22 16:52:58 +04:00
parent d649d9d450
commit feadd70f34
5 changed files with 294 additions and 12 deletions

View File

@ -15,6 +15,7 @@
package events package events
import ( import (
"fmt"
"time" "time"
applyevent "sigs.k8s.io/cli-utils/pkg/apply/event" applyevent "sigs.k8s.io/cli-utils/pkg/apply/event"
@ -56,6 +57,87 @@ type Event struct {
GenericContainerEvent GenericContainerEvent 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 // NewEvent create new event with timestamp
func NewEvent() Event { func NewEvent() Event {
return Event{ return Event{

61
pkg/events/events_test.go Normal file
View File

@ -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)
}
})
}
}

69
pkg/events/printers.go Normal file
View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -34,15 +34,19 @@ type EventProcessor interface {
type DefaultProcessor struct { type DefaultProcessor struct {
errors []error errors []error
applierChan chan<- applyevent.Event applierChan chan<- applyevent.Event
genericPrinter GenericPrinter
} }
// NewDefaultProcessor returns instance of DefaultProcessor as interface Implementation // NewDefaultProcessor returns instance of DefaultProcessor as interface Implementation
func NewDefaultProcessor(streams genericclioptions.IOStreams) EventProcessor { func NewDefaultProcessor(streams genericclioptions.IOStreams) EventProcessor {
applyCh := make(chan applyevent.Event) applyCh := make(chan applyevent.Event)
go printers.GetPrinter(printers.EventsPrinter, streams).Print(applyCh, common.DryRunNone) go printers.GetPrinter(printers.EventsPrinter, streams).Print(applyCh, common.DryRunNone)
// printer for custom airshipctl events
genericPrinter := NewGenericPrinter(log.Writer(), JSONPrinter)
return &DefaultProcessor{ return &DefaultProcessor{
errors: []error{}, errors: []error{},
applierChan: applyCh, applierChan: applyCh,
genericPrinter: genericPrinter,
} }
} }
@ -55,19 +59,16 @@ func (p *DefaultProcessor) Process(ch <-chan Event) error {
case ErrorType: case ErrorType:
log.Printf("Received error on event channel %v", e.ErrorEvent) log.Printf("Received error on event channel %v", e.ErrorEvent)
p.errors = append(p.errors, e.ErrorEvent.Error) 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: case StatusPollerType:
log.Fatalf("Processing for status poller events are not yet implemented") log.Fatalf("Processing for status poller events are not yet implemented")
case WaitType: case WaitType:
log.Fatalf("Processing for wait events are not yet implemented") log.Fatalf("Processing for wait events are not yet implemented")
default: 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) return checkErrors(p.errors)