From c75c97a8721e60904ef05d22c6a9e32cd5f34ef8 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 4 Aug 2021 14:01:58 -0400 Subject: [PATCH] Normalize event configuration (#1444) * Normalize event configuration This updates event configuration stanzas to more closely match behavior for listeners and kmses: * Singular "sink" instead of "sinks" (probably not the most correct option, but a consistent option, and one that makes sense for HCL) * Allows sinks to have the type defined at the start of the block * Moves type-specific configuration to a specific named block Co-authored-by: Jim --- go.mod | 2 +- internal/cmd/base/option_test.go | 4 +- internal/cmd/config/config.go | 77 +++++++++- internal/cmd/config/config_test.go | 145 ++++++++++++++---- internal/observability/event/event_type.go | 4 +- internal/observability/event/eventer.go | 28 ++-- .../observability/event/eventer_config.go | 4 +- .../event/eventer_config_test.go | 4 +- internal/observability/event/eventer_test.go | 27 +++- internal/observability/event/sink_config.go | 73 ++++++--- .../observability/event/sink_config_test.go | 115 ++++++++++---- internal/observability/event/sink_type.go | 2 +- internal/observability/event/testing.go | 42 +++-- internal/servers/worker/testing.go | 2 +- 14 files changed, 412 insertions(+), 117 deletions(-) diff --git a/go.mod b/go.mod index 0db6ba481..9ff51b9f5 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/bufbuild/buf v0.37.0 github.com/dhui/dktest v0.3.4 github.com/fatih/color v1.12.0 - github.com/fatih/structs v1.1.0 // indirect + github.com/fatih/structs v1.1.0 github.com/favadi/protoc-go-inject-tag v1.1.0 github.com/golang-migrate/migrate/v4 v4.14.1 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe diff --git a/internal/cmd/base/option_test.go b/internal/cmd/base/option_test.go index 500ab4a61..100381be0 100644 --- a/internal/cmd/base/option_test.go +++ b/internal/cmd/base/option_test.go @@ -37,8 +37,8 @@ func Test_GetOpts(t *testing.T) { // not a valid sink, but it doesn't need to be to test the // option is properly supported. { - Name: "test-sink", - SinkType: "Stderr", + Name: "test-sink", + Type: "Stderr", }, }, } diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index f001cd62b..72f587644 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/hcl" + "github.com/hashicorp/hcl/hcl/ast" "github.com/mitchellh/mapstructure" ) @@ -277,7 +278,6 @@ func Parse(d string) (*Config, error) { return nil, err } - // Nothing to do here right now result := New() if err := hcl.DecodeObject(result, obj); err != nil { return nil, err @@ -408,17 +408,84 @@ func Parse(d string) (*Config, error) { } } - if result.Eventing == nil { + list, ok := obj.Node.(*ast.ObjectList) + if !ok { + return nil, fmt.Errorf("error parsing: file doesn't contain a root object") + } + + eventList := list.Filter("events") + switch len(eventList.Items) { + case 0: result.Eventing = event.DefaultEventerConfig() - } else { - if result.Eventing.Sinks == nil { - result.Eventing.Sinks = []event.SinkConfig{event.DefaultSink()} + case 1: + if result.Eventing, err = parseEventing(eventList.Items[0]); err != nil { + return nil, fmt.Errorf(`error parsing "events": %w`, err) } + default: + return nil, fmt.Errorf(`too many "events" nodes (max 1, got %d)`, len(eventList.Items)) } return result, nil } +func parseEventing(eventObj *ast.ObjectItem) (*event.EventerConfig, error) { + // Decode the outside struct + var result event.EventerConfig + if err := hcl.DecodeObject(&result, eventObj.Val); err != nil { + return nil, fmt.Errorf(`error decoding "events" node: %w`, err) + } + // Now, find the sinks + eventObjType, ok := eventObj.Val.(*ast.ObjectType) + if !ok { + return nil, fmt.Errorf(`error interpreting "events" node as an object type`) + } + list := eventObjType.List + sinkList := list.Filter("sink") + // Go through each sink and decode + for i, item := range sinkList.Items { + var s event.SinkConfig + if err := hcl.DecodeObject(&s, item.Val); err != nil { + return nil, fmt.Errorf("error decoding eventer sink entry %d", i) + } + + // Fix up type and validate + switch { + case s.Type != "": + case len(item.Keys) == 1: + s.Type = event.SinkType(item.Keys[0].Token.Value().(string)) + default: + switch { + case s.StderrConfig != nil: + // If we haven't found the type any other way, they _must_ + // specify this block even though there are no config parameters + s.Type = event.StderrSink + case s.FileConfig != nil: + s.Type = event.FileSink + default: + return nil, fmt.Errorf("sink type could not be determined") + } + } + s.Type = event.SinkType(strings.ToLower(string(s.Type))) + + if s.Type == event.StderrSink && s.StderrConfig == nil { + // StderrConfig is optional as it has no values, but ensure it's + // always populated if it's the type + s.StderrConfig = new(event.StderrSinkTypeConfig) + } + + if err := s.Validate(); err != nil { + return nil, err + } + + // Append to result + result.Sinks = append(result.Sinks, s) + } + if len(result.Sinks) == 0 { + result.Sinks = []event.SinkConfig{event.DefaultSink()} + } + return &result, nil +} + // Sanitized returns a copy of the config with all values that are considered // sensitive stripped. It also strips all `*Raw` values that are mainly // used for parsing. diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 0aefefd6e..406128dd6 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -5,7 +5,6 @@ import ( "os" "testing" - "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/go-secure-stdlib/configutil" "github.com/hashicorp/go-secure-stdlib/listenerutil" @@ -329,9 +328,9 @@ func TestController_EventingConfig(t *testing.T) { tests := []struct { name string - config string + config []string wantEventerConfig *event.EventerConfig - wantErrMatch *errors.Template + wantErr string }{ { name: "default", @@ -339,11 +338,11 @@ func TestController_EventingConfig(t *testing.T) { }, { name: "audit-enabled", - config: ` + config: []string{` events { audit_enabled = true } - `, + `}, wantEventerConfig: &event.EventerConfig{ AuditEnabled: true, ObservationsEnabled: false, @@ -354,11 +353,11 @@ func TestController_EventingConfig(t *testing.T) { }, { name: "observations-enabled", - config: ` + config: []string{` events { observations_enabled = true } - `, + `}, wantEventerConfig: &event.EventerConfig{ AuditEnabled: false, ObservationsEnabled: true, @@ -368,28 +367,120 @@ func TestController_EventingConfig(t *testing.T) { }, }, { - name: "sinks-configured", - config: ` - events { + name: "no-sink-type-determined", + config: []string{ + `events { audit_enabled = false observations_enabled = true - sinks = [ - { - name = "configured-sink" + sink { + format = "cloudevents-json" + name = "configured-sink" + event_types = [ "audit", "observation" ] + } + }`, + }, + wantErr: `error parsing "events": sink type could not be determined`, + }, + { + name: "sinks-configured", + config: []string{ + `events { + audit_enabled = false + observations_enabled = true + sink { + type = "file" + format = "cloudevents-json" + name = "configured-sink" + event_types = [ "audit", "observation" ] + file { file_name = "file-name" - event_types = [ "audit", "observation" ] } - ] - } - `, + } + sink { + type = "stderr" + format = "hclog-text" + name = "stderr-sink" + event_types = [ "error" ] + } + }`, + `events { + audit_enabled = false + observations_enabled = true + sink "file" { + format = "cloudevents-json" + name = "configured-sink" + event_types = [ "audit", "observation" ] + file { + file_name = "file-name" + } + } + sink "stderr" { + format = "hclog-text" + name = "stderr-sink" + event_types = [ "error" ] + } + }`, + `events { + audit_enabled = false + observations_enabled = true + sink { + format = "cloudevents-json" + name = "configured-sink" + event_types = [ "audit", "observation" ] + file { + file_name = "file-name" + } + } + sink { + format = "hclog-text" + name = "stderr-sink" + event_types = [ "error" ] + stderr = {} + } + }`, + `{ + "events": { + "audit_enabled": false, + "observations_enabled": true, + "sink": [ + { + "type": "file", + "format": "cloudevents-json", + "name": "configured-sink", + "event_types": ["audit", "observation"], + "file": { + "file_name": "file-name" + } + }, + { + "format": "hclog-text", + "name": "stderr-sink", + "event_types": ["error"], + "stderr": {} + } + ] + } + }`, + }, wantEventerConfig: &event.EventerConfig{ AuditEnabled: false, ObservationsEnabled: true, Sinks: []event.SinkConfig{ { + Type: "file", Name: "configured-sink", - FileName: "file-name", + Format: "cloudevents-json", EventTypes: []event.Type{"audit", "observation"}, + FileConfig: &event.FileSinkTypeConfig{ + FileName: "file-name", + }, + }, + { + Type: "stderr", + Name: "stderr-sink", + Format: "hclog-text", + EventTypes: []event.Type{"error"}, + StderrConfig: &event.StderrSinkTypeConfig{}, }, }, }, @@ -398,16 +489,18 @@ func TestController_EventingConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - c, err := Parse(tt.config) - if tt.wantErrMatch != nil { + for i, conf := range tt.config { + c, err := Parse(conf) + if tt.wantErr != "" { + require.Error(err) + assert.Empty(c) + assert.Equal(tt.wantErr, err.Error(), "config %d want %q and got %q", i, tt.wantErr, err.Error()) + return + } require.NoError(err) - assert.Empty(c) - assert.Truef(errors.Match(tt.wantErrMatch, err), "want %q and got %q", tt.wantErrMatch.Code, err.Error()) - return + assert.NotEmpty(c) + assert.Equal(tt.wantEventerConfig, c.Eventing) } - require.NoError(err) - assert.NotEmpty(c) - assert.Equal(tt.wantEventerConfig, c.Eventing) }) } } diff --git a/internal/observability/event/event_type.go b/internal/observability/event/event_type.go index b1c2acd0a..b86a800e9 100644 --- a/internal/observability/event/event_type.go +++ b/internal/observability/event/event_type.go @@ -15,8 +15,8 @@ const ( SystemType Type = "system" // SysType represents system events ) -func (et Type) validate() error { - const op = "event.(Type).validate" +func (et Type) Validate() error { + const op = "event.(Type).Validate" switch et { case EveryType, ObservationType, AuditType, ErrorType, SystemType: return nil diff --git a/internal/observability/event/eventer.go b/internal/observability/event/eventer.go index b4c605e71..b29913fd9 100644 --- a/internal/observability/event/eventer.go +++ b/internal/observability/event/eventer.go @@ -203,7 +203,7 @@ func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName stri var sinkId eventlogger.NodeID var sinkNode eventlogger.Node - switch s.SinkType { + switch s.Type { case StderrSink: sinkNode = &writer.Sink{ Format: string(s.Format), @@ -214,23 +214,27 @@ func NewEventer(log hclog.Logger, serializationLock *sync.Mutex, serverName stri return nil, fmt.Errorf("%s: %w", op, err) } sinkId = eventlogger.NodeID(id) - default: - if _, found := allSinkFilenames[s.Path+s.FileName]; found { - return nil, fmt.Errorf("%s: duplicate file sink: %s %s", op, s.Path, s.FileName) + case FileSink: + fsc := s.FileConfig + if _, found := allSinkFilenames[fsc.Path+fsc.FileName]; found { + return nil, fmt.Errorf("%s: duplicate file sink: %s %s: %w", op, fsc.Path, fsc.FileName, ErrInvalidParameter) } + allSinkFilenames[fsc.Path+fsc.FileName] = true sinkNode = &eventlogger.FileSink{ Format: string(s.Format), - Path: s.Path, - FileName: s.FileName, - MaxBytes: s.RotateBytes, - MaxDuration: s.RotateDuration, - MaxFiles: s.RotateMaxFiles, + Path: fsc.Path, + FileName: fsc.FileName, + MaxBytes: fsc.RotateBytes, + MaxDuration: fsc.RotateDuration, + MaxFiles: fsc.RotateMaxFiles, } - id, err := NewId(fmt.Sprintf("file_%s_%s_", s.Path, s.FileName)) + id, err := NewId(fmt.Sprintf("file_%s_%s_", fsc.Path, fsc.FileName)) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } sinkId = eventlogger.NodeID(id) + default: + return nil, fmt.Errorf("%s: unknown sink type %s", op, s.Type) } err = e.broker.RegisterNode(sinkId, sinkNode) if err != nil { @@ -474,7 +478,7 @@ func DefaultSink() SinkConfig { Name: "default", EventTypes: []Type{EveryType}, Format: JSONSinkFormat, - SinkType: StderrSink, + Type: StderrSink, } } @@ -610,7 +614,7 @@ func (e *Eventer) StandardWriter(ctx context.Context, typ Type) (io.Writer, erro if typ == "" { return nil, fmt.Errorf("%s: missing type: %w", op, ErrInvalidParameter) } - if err := typ.validate(); err != nil { + if err := typ.Validate(); err != nil { return nil, fmt.Errorf("%s: %w", op, err) } newEventer := *e diff --git a/internal/observability/event/eventer_config.go b/internal/observability/event/eventer_config.go index 635510d7d..90e59a982 100644 --- a/internal/observability/event/eventer_config.go +++ b/internal/observability/event/eventer_config.go @@ -9,7 +9,7 @@ type EventerConfig struct { AuditEnabled bool `hcl:"audit_enabled"` // AuditEnabled specifies if audit events should be emitted. ObservationsEnabled bool `hcl:"observations_enabled"` // ObservationsEnabled specifies if observation events should be emitted. SysEventsEnabled bool `hcl:"sysevents_enabled"` // SysEventsEnabled specifies if sysevents should be emitted. - Sinks []SinkConfig `hcl:"sinks"` // Sinks are all the configured sinks + Sinks []SinkConfig `hcl:"-"` // Sinks are all the configured sinks } // Validate will Validate the config. A config isn't required to have any @@ -17,7 +17,7 @@ type EventerConfig struct { func (c *EventerConfig) Validate() error { const op = "event.(EventerConfig).Validate" for i, s := range c.Sinks { - if err := s.validate(); err != nil { + if err := s.Validate(); err != nil { return fmt.Errorf("%s: sink %d is invalid: %w", op, i, err) } } diff --git a/internal/observability/event/eventer_config_test.go b/internal/observability/event/eventer_config_test.go index 1b63133cb..55f935bba 100644 --- a/internal/observability/event/eventer_config_test.go +++ b/internal/observability/event/eventer_config_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestEventerConfig_validate(t *testing.T) { +func TestEventerConfig_Validate(t *testing.T) { tests := []struct { name string c EventerConfig @@ -19,7 +19,7 @@ func TestEventerConfig_validate(t *testing.T) { c: EventerConfig{ Sinks: []SinkConfig{ { - SinkType: "invalid", + Type: "invalid", }, }, }, diff --git a/internal/observability/event/eventer_test.go b/internal/observability/event/eventer_test.go index dae6cf3ee..7da2b175e 100644 --- a/internal/observability/event/eventer_test.go +++ b/internal/observability/event/eventer_test.go @@ -113,7 +113,7 @@ func Test_InitSysEventer(t *testing.T) { Name: "default", EventTypes: []Type{EveryType}, Format: JSONSinkFormat, - SinkType: StderrSink, + Type: StderrSink, }, }, }, @@ -396,6 +396,29 @@ func Test_NewEventer(t *testing.T) { lock: testLock, wantErrIs: ErrInvalidParameter, }, + { + name: "dup-sink-filename", + config: func() EventerConfig { + dupFileConfig := TestEventerConfig(t, "dup-sink-filename") + dupFileConfig.EventerConfig.Sinks = append(dupFileConfig.EventerConfig.Sinks, + SinkConfig{ + Name: "err-file-sink", + Type: FileSink, + EventTypes: []Type{ErrorType}, + Format: JSONSinkFormat, + FileConfig: &FileSinkTypeConfig{ + Path: "./", + FileName: dupFileConfig.ErrorEvents.Name(), + }, + }, + ) + return dupFileConfig.EventerConfig + }(), + logger: testLogger, + lock: testLock, + serverName: "dup-sink-filename", + wantErrIs: ErrInvalidParameter, + }, { name: "success-with-default-config", config: EventerConfig{}, @@ -410,7 +433,7 @@ func Test_NewEventer(t *testing.T) { Name: "default", EventTypes: []Type{EveryType}, Format: JSONSinkFormat, - SinkType: StderrSink, + Type: StderrSink, }, }, }, diff --git a/internal/observability/event/sink_config.go b/internal/observability/event/sink_config.go index a3b8bbbb2..63dfacdd6 100644 --- a/internal/observability/event/sink_config.go +++ b/internal/observability/event/sink_config.go @@ -7,31 +7,54 @@ import ( // SinkConfig defines the configuration for a Eventer sink type SinkConfig struct { - Name string `hcl:"name"` // Name defines a name for the sink. - Description string `hcl:"description"` // Description defines a description for the sink. - EventTypes []Type `hcl:"event_types"` // EventTypes defines a list of event types that will be sent to the sink. See the docs for EventTypes for a list of accepted values. - EventSourceUrl string `hcl:"event_source_url"` // EventSource defines an optional event source URL for the sink. If not defined a default source will be composed of the https://hashicorp.com/boundary.io/ServerName/Path/FileName - AllowFilters []string `hcl:"allow_filters"` // AllowFilters define a set predicates for including an event in the sink. If any filter matches, the event will be included. The filter should be in a format supported by hashicorp/go-bexpr. - DenyFilters []string `hcl:"deny_filters"` // DenyFilters define a set predicates for excluding an event in the sink. If any filter matches, the event will be excluded. The filter should be in a format supported by hashicorp/go-bexpr. - SinkType SinkType `hcl:"sink_type"` // SinkType defines the type of sink (StderrSink or FileSink) - Format SinkFormat `hcl:"format"` // Format defines the format for the sink (JSONSinkFormat or TextSinkFormat) - Path string `hcl:"path"` // Path defines the file path for the sink - FileName string `hcl:"file_name"` // FileName defines the file name for the sink - RotateBytes int `hcl:"rotate_bytes"` // RotateByes defines the number of bytes that should trigger rotation of a FileSink - RotateDuration time.Duration `hcl:"rotate_duration"` // RotateDuration defines how often a FileSink should be rotated - RotateMaxFiles int `hcl:"rotate_max_files"` // RotateMaxFiles defines how may historical rotated files should be kept for a FileSink + Name string `hcl:"name"` // Name defines a name for the sink. + Description string `hcl:"description"` // Description defines a description for the sink. + EventTypes []Type `hcl:"event_types"` // EventTypes defines a list of event types that will be sent to the sink. See the docs for EventTypes for a list of accepted values. + EventSourceUrl string `hcl:"event_source_url"` // EventSource defines an optional event source URL for the sink. If not defined a default source will be composed of the https://hashicorp.com/boundary.io/ServerName/Path/FileName. + AllowFilters []string `hcl:"allow_filters"` // AllowFilters define a set predicates for including an event in the sink. If any filter matches, the event will be included. The filter should be in a format supported by hashicorp/go-bexpr. + DenyFilters []string `hcl:"deny_filters"` // DenyFilters define a set predicates for excluding an event in the sink. If any filter matches, the event will be excluded. The filter should be in a format supported by hashicorp/go-bexpr. + Format SinkFormat `hcl:"format"` // Format defines the format for the sink (JSONSinkFormat or TextSinkFormat). + Type SinkType `hcl:"type"` // Type defines the type of sink (StderrSink or FileSink). + StderrConfig *StderrSinkTypeConfig `hcl:"stderr"` // StderrConfig defines parameters for a stderr output. + FileConfig *FileSinkTypeConfig `hcl:"file"` // FileConfig defines parameters for a file output. } -func (sc *SinkConfig) validate() error { - const op = "event.(SinkConfig).validate" - if err := sc.SinkType.validate(); err != nil { +func (sc *SinkConfig) Validate() error { + const op = "event.(SinkConfig).Validate" + if err := sc.Type.Validate(); err != nil { return fmt.Errorf("%s: %w", op, err) } if err := sc.Format.Validate(); err != nil { return fmt.Errorf("%s: %w", op, err) } - if sc.SinkType == FileSink && sc.FileName == "" { - return fmt.Errorf("%s: missing sink file name: %w", op, ErrInvalidParameter) + + var foundSinkTypeConfigs int + if sc.StderrConfig != nil { + foundSinkTypeConfigs++ + } + if sc.FileConfig != nil { + foundSinkTypeConfigs++ + } + if foundSinkTypeConfigs > 1 { + return fmt.Errorf("%s: too many sink type config blocks: %w", op, ErrInvalidParameter) + } + + switch sc.Type { + case StderrSink: + // It's okay for StderrConfig to be nil if the type is stderr, but it's + // not okay for something else to be set + if foundSinkTypeConfigs == 1 && sc.StderrConfig == nil { + return fmt.Errorf("%s: mismatch between sink type and sink configuration block: %w", op, ErrInvalidParameter) + } + case FileSink: + // Unlike in the stderr case, this can't be nil, so if it's not nil + // we've now verified it's the only block populated + if sc.FileConfig == nil { + return fmt.Errorf(`%s: missing "file" block: %w`, op, ErrInvalidParameter) + } + if sc.FileConfig.FileName == "" { + return fmt.Errorf("%s: missing file name: %w", op, ErrInvalidParameter) + } } if sc.Name == "" { return fmt.Errorf("%s: missing sink name: %w", op, ErrInvalidParameter) @@ -40,13 +63,25 @@ func (sc *SinkConfig) validate() error { return fmt.Errorf("%s: missing event types: %w", op, ErrInvalidParameter) } for _, et := range sc.EventTypes { - if err := et.validate(); err != nil { + if err := et.Validate(); err != nil { return fmt.Errorf("%s: %w", op, err) } } return nil } +// StderrSinkTypeConfig contains configuration structures for file sink types +type StderrSinkTypeConfig struct{} + +// FileSinkTypeConfig contains configuration structures for file sink types +type FileSinkTypeConfig struct { + Path string `hcl:"path" mapstructure:"path"` // Path defines the file path for the sink + FileName string `hcl:"file_name" mapstructure:"file_name"` // FileName defines the file name for the sink + RotateBytes int `hcl:"rotate_bytes" mapstructure:"rotate_bytes"` // RotateByes defines the number of bytes that should trigger rotation of a FileSink + RotateDuration time.Duration `hcl:"rotate_duration" mapstructure:"rotate_duration"` // RotateDuration defines how often a FileSink should be rotated + RotateMaxFiles int `hcl:"rotate_max_files" mapstructure:"rotate_max_files"` // RotateMaxFiles defines how may historical rotated files should be kept for a FileSink +} + // FilterType defines a type for filters (allow or deny) type FilterType string diff --git a/internal/observability/event/sink_config_test.go b/internal/observability/event/sink_config_test.go index 530770a1e..ec6f5fc74 100644 --- a/internal/observability/event/sink_config_test.go +++ b/internal/observability/event/sink_config_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSinkConfig_validate(t *testing.T) { +func TestSinkConfig_Validate(t *testing.T) { t.Parallel() tests := []struct { name string @@ -19,9 +19,11 @@ func TestSinkConfig_validate(t *testing.T) { name: "missing-name", sc: SinkConfig{ EventTypes: []Type{EveryType}, - SinkType: FileSink, - FileName: "tmp.file", - Format: JSONSinkFormat, + Type: FileSink, + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, + Format: JSONSinkFormat, }, wantErrIs: ErrInvalidParameter, wantErrContains: "missing sink name", @@ -29,10 +31,12 @@ func TestSinkConfig_validate(t *testing.T) { { name: "missing-EventType", sc: SinkConfig{ - Name: "sink-name", - SinkType: FileSink, - FileName: "tmp.file", - Format: JSONSinkFormat, + Name: "sink-name", + Type: FileSink, + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, + Format: JSONSinkFormat, }, wantErrIs: ErrInvalidParameter, wantErrContains: "missing event types", @@ -42,9 +46,11 @@ func TestSinkConfig_validate(t *testing.T) { sc: SinkConfig{ Name: "sink-name", EventTypes: []Type{"invalid"}, - SinkType: FileSink, - FileName: "tmp.file", - Format: JSONSinkFormat, + Type: FileSink, + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, + Format: JSONSinkFormat, }, wantErrIs: ErrInvalidParameter, wantErrContains: "not a valid event type", @@ -54,8 +60,10 @@ func TestSinkConfig_validate(t *testing.T) { sc: SinkConfig{ Name: "sink-name", EventTypes: []Type{EveryType}, - FileName: "tmp.file", - Format: JSONSinkFormat, + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, + Format: JSONSinkFormat, }, wantErrIs: ErrInvalidParameter, wantErrContains: "not a valid sink type", @@ -65,9 +73,11 @@ func TestSinkConfig_validate(t *testing.T) { sc: SinkConfig{ Name: "sink-name", EventTypes: []Type{EveryType}, - SinkType: "invalid", - FileName: "tmp.file", - Format: JSONSinkFormat, + Type: "invalid", + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, + Format: JSONSinkFormat, }, wantErrIs: ErrInvalidParameter, wantErrContains: "not a valid sink type", @@ -76,9 +86,11 @@ func TestSinkConfig_validate(t *testing.T) { name: "missing-format", sc: SinkConfig{ Name: "sink-name", - SinkType: FileSink, + Type: FileSink, EventTypes: []Type{EveryType}, - FileName: "tmp.file", + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, }, wantErrIs: ErrInvalidParameter, wantErrContains: "not a valid sink format", @@ -88,9 +100,11 @@ func TestSinkConfig_validate(t *testing.T) { sc: SinkConfig{ Name: "sink-name", Format: "invalid", - SinkType: FileSink, + Type: FileSink, EventTypes: []Type{EveryType}, - FileName: "tmp.file", + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, }, wantErrIs: ErrInvalidParameter, wantErrContains: "not a valid sink format", @@ -99,27 +113,76 @@ func TestSinkConfig_validate(t *testing.T) { name: "file-sink-with-no-file-name", sc: SinkConfig{ EventTypes: []Type{EveryType}, - SinkType: FileSink, + Type: FileSink, Format: JSONSinkFormat, + FileConfig: &FileSinkTypeConfig{}, }, wantErrIs: ErrInvalidParameter, - wantErrContains: "missing sink file name", + wantErrContains: "missing file name", + }, + { + name: "type mismatch file type stderr config", + sc: SinkConfig{ + EventTypes: []Type{EveryType}, + Type: FileSink, + Format: JSONSinkFormat, + StderrConfig: &StderrSinkTypeConfig{}, + }, + wantErrIs: ErrInvalidParameter, + wantErrContains: `missing "file" block`, + }, + { + name: "type mismatch stderr type file config", + sc: SinkConfig{ + EventTypes: []Type{EveryType}, + Type: StderrSink, + Format: JSONSinkFormat, + FileConfig: &FileSinkTypeConfig{}, + }, + wantErrIs: ErrInvalidParameter, + wantErrContains: `mismatch between sink type and sink configuration block`, + }, + { + name: "type mismatch both types file config", + sc: SinkConfig{ + EventTypes: []Type{EveryType}, + Type: FileSink, + Format: JSONSinkFormat, + StderrConfig: &StderrSinkTypeConfig{}, + FileConfig: &FileSinkTypeConfig{}, + }, + wantErrIs: ErrInvalidParameter, + wantErrContains: `too many sink type config blocks`, + }, + { + name: "type mismatch both types stderr config", + sc: SinkConfig{ + EventTypes: []Type{EveryType}, + Type: StderrSink, + Format: JSONSinkFormat, + StderrConfig: &StderrSinkTypeConfig{}, + FileConfig: &FileSinkTypeConfig{}, + }, + wantErrIs: ErrInvalidParameter, + wantErrContains: `too many sink type config blocks`, }, { name: "valid", sc: SinkConfig{ Name: "valid", EventTypes: []Type{EveryType}, - SinkType: FileSink, - FileName: "tmp.file", - Format: JSONSinkFormat, + Type: FileSink, + FileConfig: &FileSinkTypeConfig{ + FileName: "tmp.file", + }, + Format: JSONSinkFormat, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - err := tt.sc.validate() + err := tt.sc.Validate() if tt.wantErrIs != nil { require.Error(err) assert.ErrorIs(err, tt.wantErrIs) diff --git a/internal/observability/event/sink_type.go b/internal/observability/event/sink_type.go index 1b53e6ad4..a622e0c3e 100644 --- a/internal/observability/event/sink_type.go +++ b/internal/observability/event/sink_type.go @@ -11,7 +11,7 @@ const ( type SinkType string // SinkType defines the type of sink in a config stanza (file, stderr) -func (t SinkType) validate() error { +func (t SinkType) Validate() error { const op = "event.(SinkType).validate" switch t { case StderrSink, FileSink: diff --git a/internal/observability/event/testing.go b/internal/observability/event/testing.go index 3b7333c72..3564033fd 100644 --- a/internal/observability/event/testing.go +++ b/internal/observability/event/testing.go @@ -79,25 +79,29 @@ func TestEventerConfig(t *testing.T, testName string, opt ...Option) TestConfig Sinks: []SinkConfig{ { Name: "every-type-file-sink", - SinkType: FileSink, + Type: FileSink, EventTypes: []Type{EveryType}, Format: opts.withSinkFormat, - Path: "./", - FileName: tmpAllFile.Name(), + FileConfig: &FileSinkTypeConfig{ + Path: "./", + FileName: tmpAllFile.Name(), + }, }, { Name: "stderr", - SinkType: StderrSink, + Type: StderrSink, EventTypes: []Type{EveryType}, Format: opts.withSinkFormat, }, { Name: "err-file-sink", - SinkType: FileSink, + Type: FileSink, EventTypes: []Type{ErrorType}, Format: opts.withSinkFormat, - Path: "./", - FileName: tmpErrFile.Name(), + FileConfig: &FileSinkTypeConfig{ + Path: "./", + FileName: tmpErrFile.Name(), + }, }, }, }, @@ -112,11 +116,13 @@ func TestEventerConfig(t *testing.T, testName string, opt ...Option) TestConfig }) c.EventerConfig.Sinks = append(c.EventerConfig.Sinks, SinkConfig{ Name: "audit-file-sink", - SinkType: FileSink, + Type: FileSink, EventTypes: []Type{AuditType}, Format: opts.withSinkFormat, - Path: "./", - FileName: tmpFile.Name(), + FileConfig: &FileSinkTypeConfig{ + Path: "./", + FileName: tmpFile.Name(), + }, }) c.AuditEvents = tmpFile } @@ -128,11 +134,13 @@ func TestEventerConfig(t *testing.T, testName string, opt ...Option) TestConfig }) c.EventerConfig.Sinks = append(c.EventerConfig.Sinks, SinkConfig{ Name: "err-observation-sink", - SinkType: FileSink, + Type: FileSink, EventTypes: []Type{ObservationType}, Format: opts.withSinkFormat, - Path: "./", - FileName: tmpFile.Name(), + FileConfig: &FileSinkTypeConfig{ + Path: "./", + FileName: tmpFile.Name(), + }, }) c.ObservationEvents = tmpFile } @@ -144,11 +152,13 @@ func TestEventerConfig(t *testing.T, testName string, opt ...Option) TestConfig }) c.EventerConfig.Sinks = append(c.EventerConfig.Sinks, SinkConfig{ Name: "err-sysevents-sink", - SinkType: FileSink, + Type: FileSink, EventTypes: []Type{SystemType}, Format: opts.withSinkFormat, - Path: "./", - FileName: tmpFile.Name(), + FileConfig: &FileSinkTypeConfig{ + Path: "./", + FileName: tmpFile.Name(), + }, }) } return c diff --git a/internal/servers/worker/testing.go b/internal/servers/worker/testing.go index 44025ed9b..66aa29155 100644 --- a/internal/servers/worker/testing.go +++ b/internal/servers/worker/testing.go @@ -240,7 +240,7 @@ func NewTestWorker(t *testing.T, opts *TestWorkerOpts) *TestWorker { } tw.name = opts.Config.Worker.Name - if err := tw.b.SetupEventing(tw.b.Logger, tw.b.StderrLock, opts.Config.Worker.Name+"/test-worker", base.WithEventerConfig(opts.Config.Eventing)); err != nil { + if err := tw.b.SetupEventing(tw.b.Logger, tw.b.StderrLock, opts.Config.Worker.Name, base.WithEventerConfig(opts.Config.Eventing)); err != nil { t.Fatal(err) }