mirror of https://github.com/hashicorp/boundary
commit
d6ef2732e7
@ -0,0 +1,231 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
eventerKey key = iota
|
||||
requestInfoKey
|
||||
)
|
||||
|
||||
// NewEventerContext will return a context containing a value of the provided Eventer
|
||||
func NewEventerContext(ctx context.Context, eventer *Eventer) (context.Context, error) {
|
||||
const op = "event.NewEventerContext"
|
||||
if ctx == nil {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing context")
|
||||
}
|
||||
if eventer == nil {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing eventer")
|
||||
}
|
||||
return context.WithValue(ctx, eventerKey, eventer), nil
|
||||
}
|
||||
|
||||
// EventerFromContext attempts to get the eventer value from the context provided
|
||||
func EventerFromContext(ctx context.Context) (*Eventer, bool) {
|
||||
if ctx == nil {
|
||||
return nil, false
|
||||
}
|
||||
eventer, ok := ctx.Value(eventerKey).(*Eventer)
|
||||
return eventer, ok
|
||||
}
|
||||
|
||||
// NewRequestInfoContext will return a context containing a value for the
|
||||
// provided RequestInfo
|
||||
func NewRequestInfoContext(ctx context.Context, info *RequestInfo) (context.Context, error) {
|
||||
const op = "event.NewRequestInfoContext"
|
||||
if ctx == nil {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing context")
|
||||
}
|
||||
if info == nil {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing request info")
|
||||
}
|
||||
if info.Id == "" {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing request info id")
|
||||
}
|
||||
return context.WithValue(ctx, requestInfoKey, info), nil
|
||||
}
|
||||
|
||||
// RequestInfoFromContext attempts to get the RequestInfo value from the context
|
||||
// provided
|
||||
func RequestInfoFromContext(ctx context.Context) (*RequestInfo, bool) {
|
||||
if ctx == nil {
|
||||
return nil, false
|
||||
}
|
||||
reqInfo, ok := ctx.Value(requestInfoKey).(*RequestInfo)
|
||||
return reqInfo, ok
|
||||
}
|
||||
|
||||
// WriteObservation will write an observation event. It will first check the
|
||||
// ctx for an eventer, then try event.SysEventer() and if no eventer can be
|
||||
// found an error is returned.
|
||||
//
|
||||
// At least one and any combination of the supported options may be used:
|
||||
// WithHeader, WithDetails, WithId, WithFlush and WithRequestInfo. All other
|
||||
// options are ignored.
|
||||
func WriteObservation(ctx context.Context, caller Op, opt ...Option) error {
|
||||
const op = "event.WriteObservation"
|
||||
if ctx == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing context")
|
||||
}
|
||||
if caller == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing operation")
|
||||
}
|
||||
eventer, ok := EventerFromContext(ctx)
|
||||
if !ok {
|
||||
eventer = SysEventer()
|
||||
if eventer == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing both context and system eventer")
|
||||
}
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withDetails == nil && opts.withHeader == nil && !opts.withFlush {
|
||||
return errors.New(errors.InvalidParameter, op, "you must specify either header or details options for an event payload")
|
||||
}
|
||||
if opts.withRequestInfo == nil {
|
||||
var err error
|
||||
if opt, err = addCtxOptions(ctx, opt...); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
}
|
||||
e, err := newObservation(caller, opt...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
if err := eventer.writeObservation(ctx, e); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteError will write an error event. It will first check the
|
||||
// ctx for an eventer, then try event.SysEventer() and if no eventer can be
|
||||
// found an hclog.Logger will be created and used.
|
||||
//
|
||||
// The options WithId and WithRequestInfo are supported and all other options
|
||||
// are ignored.
|
||||
func WriteError(ctx context.Context, caller Op, e error, opt ...Option) {
|
||||
const op = "event.WriteError"
|
||||
// EventerFromContext will handle a nil ctx appropriately. If e or caller is
|
||||
// missing, newError(...) will handle them appropriately.
|
||||
eventer, ok := EventerFromContext(ctx)
|
||||
if !ok {
|
||||
eventer = SysEventer()
|
||||
if eventer == nil {
|
||||
logger := hclog.New(nil)
|
||||
logger.Error(fmt.Sprintf("%s: no eventer available to write error: %s", op, e))
|
||||
return
|
||||
}
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withRequestInfo == nil {
|
||||
var err error
|
||||
if opt, err = addCtxOptions(ctx, opt...); err != nil {
|
||||
eventer.logger.Error(errors.Wrap(err, op).Error())
|
||||
eventer.logger.Error(fmt.Sprintf("%s: unable to process context options to write error: %s", op, e))
|
||||
return
|
||||
}
|
||||
}
|
||||
ev, err := newError(caller, e, opt...)
|
||||
if err != nil {
|
||||
eventer.logger.Error(errors.Wrap(err, op).Error())
|
||||
eventer.logger.Error(fmt.Sprintf("%s: unable to create new error to write error: %s", op, e))
|
||||
return
|
||||
}
|
||||
if err := eventer.writeError(ctx, ev); err != nil {
|
||||
eventer.logger.Error(errors.Wrap(err, op).Error())
|
||||
eventer.logger.Error(fmt.Sprintf("%s: unable to write error: %s", op, e))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAudit will write an audit event. It will first check the ctx for an
|
||||
// eventer, then try event.SysEventer() and if no eventer can be found an error
|
||||
// is returned.
|
||||
//
|
||||
// At least one and any combination of the supported options may be used:
|
||||
// WithRequest, WithResponse, WithAuth, WithId, WithFlush and WithRequestInfo.
|
||||
// All other options are ignored.
|
||||
func WriteAudit(ctx context.Context, caller Op, opt ...Option) error {
|
||||
const op = "event.WriteAudit"
|
||||
if ctx == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing context")
|
||||
}
|
||||
if caller == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing operation")
|
||||
}
|
||||
eventer, ok := EventerFromContext(ctx)
|
||||
if !ok {
|
||||
eventer = SysEventer()
|
||||
if eventer == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing both context and system eventer")
|
||||
}
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withRequestInfo == nil {
|
||||
var err error
|
||||
if opt, err = addCtxOptions(ctx, opt...); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
}
|
||||
e, err := newAudit(caller, opt...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
if err := eventer.writeAudit(ctx, e); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addCtxOptions(ctx context.Context, opt ...Option) ([]Option, error) {
|
||||
const op = "event.addCtxOptions"
|
||||
opts := getOpts(opt...)
|
||||
retOpts := make([]Option, 0, len(opt))
|
||||
retOpts = append(retOpts, opt...)
|
||||
if opts.withRequestInfo == nil {
|
||||
reqInfo, ok := RequestInfoFromContext(ctx)
|
||||
if !ok {
|
||||
// there's no RequestInfo, so there's no id associated with the
|
||||
// observation and we'll generate one and flush the observation
|
||||
// since there will never be another with the same id
|
||||
id, err := newId(string(ObservationType))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
retOpts = append(retOpts, WithId(id))
|
||||
if !opts.withFlush {
|
||||
retOpts = append(retOpts, WithFlush())
|
||||
}
|
||||
return retOpts, nil
|
||||
}
|
||||
retOpts = append(retOpts, WithRequestInfo(reqInfo))
|
||||
if reqInfo.Id != "" {
|
||||
retOpts = append(retOpts, WithId(reqInfo.Id))
|
||||
}
|
||||
switch reqInfo.Id {
|
||||
case "":
|
||||
// there's no RequestInfo.Id associated with the observation, so we'll
|
||||
// generate one and flush the observation since there will never be
|
||||
// another with the same id
|
||||
id, err := newId(string(ObservationType))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
retOpts = append(retOpts, WithId(id))
|
||||
if !opts.withFlush {
|
||||
retOpts = append(retOpts, WithFlush())
|
||||
}
|
||||
return retOpts, nil
|
||||
default:
|
||||
retOpts = append(retOpts, WithId(reqInfo.Id))
|
||||
}
|
||||
}
|
||||
return retOpts, nil
|
||||
}
|
||||
@ -0,0 +1,903 @@
|
||||
package event_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/hosts"
|
||||
"github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes"
|
||||
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
|
||||
"github.com/hashicorp/boundary/internal/host/static"
|
||||
"github.com/hashicorp/boundary/internal/observability/event"
|
||||
"github.com/hashicorp/boundary/internal/types/scope"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const apiRequest = "APIRequest"
|
||||
|
||||
const testAuditVersion = "v0.1"
|
||||
const testErrorVersion = "v0.1"
|
||||
const testObservationVersion = "v0.1"
|
||||
|
||||
type testAudit struct {
|
||||
Id string `json:"id"` // std audit/boundary field
|
||||
Version string `json:"version"` // std audit/boundary field
|
||||
Type string `json:"type"` // std audit field
|
||||
Timestamp time.Time `json:"timestamp"` // std audit field
|
||||
RequestInfo *event.RequestInfo `json:"request_info,omitempty"` // boundary field
|
||||
Auth *event.Auth `json:"auth,omitempty"` // std audit field
|
||||
Request *event.Request `json:"request,omitempty"` // std audit field
|
||||
Response *event.Response `json:"response,omitempty"` // std audit field
|
||||
SerializedHMAC string `json:"serialized_hmac"` // boundary field
|
||||
Flush bool `json:"-"`
|
||||
}
|
||||
|
||||
func Test_NewRequestInfoContext(t *testing.T) {
|
||||
testInfo := event.TestRequestInfo(t)
|
||||
testInfoMissingId := event.TestRequestInfo(t)
|
||||
testInfoMissingId.Id = ""
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
requestInfo *event.RequestInfo
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-ctx",
|
||||
requestInfo: testInfo,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing context",
|
||||
},
|
||||
{
|
||||
name: "missing-request-info",
|
||||
ctx: context.Background(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing request info",
|
||||
},
|
||||
{
|
||||
name: "missing-request-info-id",
|
||||
ctx: context.Background(),
|
||||
requestInfo: testInfoMissingId,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing request info id",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
ctx: context.Background(),
|
||||
requestInfo: testInfo,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
ctx, err := event.NewRequestInfoContext(tt.ctx, tt.requestInfo)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Errorf(err, "should have gotten an error")
|
||||
assert.Nilf(ctx, "context should be nil")
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoErrorf(err, "should not have been a problem getting the request info")
|
||||
require.NotNilf(ctx, "cxt returned shouldn't be nil")
|
||||
got, ok := event.RequestInfoFromContext(ctx)
|
||||
require.Truef(ok, "should be ok to get the request info")
|
||||
assert.Equal(tt.requestInfo, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RequestInfoFromContext(t *testing.T) {
|
||||
testInfo := event.TestRequestInfo(t)
|
||||
|
||||
testCtx, err := event.NewRequestInfoContext(context.Background(), testInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
wantInfo *event.RequestInfo
|
||||
wantNotOk bool
|
||||
}{
|
||||
{
|
||||
name: "missing-ctx",
|
||||
wantNotOk: true,
|
||||
},
|
||||
{
|
||||
name: "no-request-info",
|
||||
ctx: context.Background(),
|
||||
wantNotOk: true,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
wantInfo: testInfo,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, ok := event.RequestInfoFromContext(tt.ctx)
|
||||
if tt.wantNotOk {
|
||||
require.Falsef(ok, "should not have returned ok for the request info")
|
||||
assert.Nilf(got, "should not have returned %q request info", got)
|
||||
return
|
||||
}
|
||||
require.Truef(ok, "should have been okay for getting the request info")
|
||||
require.NotNilf(got, "request info should not be nil")
|
||||
assert.Equal(tt.wantInfo, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewEventerContext(t *testing.T) {
|
||||
testSetup := event.TestEventerConfig(t, "Test_NewEventerContext")
|
||||
testEventer, err := event.NewEventer(hclog.Default(), testSetup.EventerConfig)
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
eventer *event.Eventer
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-ctx",
|
||||
eventer: testEventer,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing context",
|
||||
},
|
||||
{
|
||||
name: "missing-eventer",
|
||||
ctx: context.Background(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing eventer",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
ctx: context.Background(),
|
||||
eventer: testEventer,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
ctx, err := event.NewEventerContext(tt.ctx, tt.eventer)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Errorf(err, "should have gotten an error")
|
||||
assert.Nilf(ctx, "context should be nil")
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoErrorf(err, "should not have been a problem getting the eventer")
|
||||
require.NotNilf(ctx, "cxt returned shouldn't be nil")
|
||||
got, ok := event.EventerFromContext(ctx)
|
||||
require.Truef(ok, "should be ok to get the eventer")
|
||||
assert.Equal(tt.eventer, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_EventerFromContext(t *testing.T) {
|
||||
testSetup := event.TestEventerConfig(t, "Test_EventerFromContext")
|
||||
|
||||
testEventer, err := event.NewEventer(hclog.Default(), testSetup.EventerConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
testEventerCtx, err := event.NewEventerContext(context.Background(), testEventer)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
wantEventer *event.Eventer
|
||||
wantNotOk bool
|
||||
}{
|
||||
{
|
||||
name: "missing-ctx",
|
||||
wantNotOk: true,
|
||||
},
|
||||
{
|
||||
name: "no-eventer",
|
||||
ctx: context.Background(),
|
||||
wantNotOk: true,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testEventerCtx,
|
||||
wantEventer: testEventer,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, ok := event.EventerFromContext(tt.ctx)
|
||||
if tt.wantNotOk {
|
||||
require.Falsef(ok, "should not have returned ok for the eventer")
|
||||
assert.Nilf(got, "should not have returned %q eventer", got)
|
||||
return
|
||||
}
|
||||
require.Truef(ok, "should have been okay for getting an eventer")
|
||||
require.NotNilf(got, "eventer should not be nil")
|
||||
assert.Equal(tt.wantEventer, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_WriteObservation(t *testing.T) {
|
||||
// this test and its subtests cannot be run in parallel because of it's
|
||||
// dependency on the sysEventer
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test",
|
||||
})
|
||||
|
||||
c := event.TestEventerConfig(t, "WriteObservation")
|
||||
|
||||
e, err := event.NewEventer(logger, c.EventerConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
info := &event.RequestInfo{Id: "867-5309"}
|
||||
|
||||
testCtx, err := event.NewEventerContext(context.Background(), e)
|
||||
require.NoError(t, err)
|
||||
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCtxNoInfoId, err := event.NewEventerContext(context.Background(), e)
|
||||
require.NoError(t, err)
|
||||
noId := &event.RequestInfo{Id: "867-5309"}
|
||||
testCtxNoInfoId, err = event.NewRequestInfoContext(testCtxNoInfoId, noId)
|
||||
require.NoError(t, err)
|
||||
noId.Id = ""
|
||||
|
||||
type observationPayload struct {
|
||||
header map[string]interface{}
|
||||
details map[string]interface{}
|
||||
}
|
||||
|
||||
testPayloads := []observationPayload{
|
||||
{
|
||||
header: map[string]interface{}{
|
||||
"name": "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: map[string]interface{}{
|
||||
"list": []string{"1", "2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
details: map[string]interface{}{
|
||||
"file": "temp-file.txt",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testWantHeader := map[string]interface{}{
|
||||
"name": "bar",
|
||||
"list": []string{"1", "2"},
|
||||
}
|
||||
|
||||
testWantDetails := map[string]interface{}{
|
||||
"file": "temp-file.txt",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
noOperation bool
|
||||
noFlush bool
|
||||
observationPayload []observationPayload
|
||||
header map[string]interface{}
|
||||
details map[string]interface{}
|
||||
ctx context.Context
|
||||
observationSinkFileName string
|
||||
setup func() error
|
||||
cleanup func()
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "no-info-id",
|
||||
noFlush: true,
|
||||
ctx: testCtxNoInfoId,
|
||||
observationPayload: []observationPayload{
|
||||
{
|
||||
header: map[string]interface{}{
|
||||
"name": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
header: map[string]interface{}{
|
||||
"name": "bar",
|
||||
},
|
||||
observationSinkFileName: c.AllEvents.Name(),
|
||||
setup: func() error {
|
||||
return event.InitSysEventer(hclog.Default(), c.EventerConfig)
|
||||
},
|
||||
cleanup: func() { event.TestResetSystEventer(t) },
|
||||
},
|
||||
{
|
||||
name: "missing-ctx",
|
||||
observationPayload: testPayloads,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing context",
|
||||
},
|
||||
{
|
||||
name: "missing-op",
|
||||
ctx: testCtx,
|
||||
noOperation: true,
|
||||
observationPayload: testPayloads,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing operation",
|
||||
},
|
||||
{
|
||||
name: "no-header-or-details-in-payload",
|
||||
noFlush: true,
|
||||
ctx: testCtx,
|
||||
observationPayload: []observationPayload{
|
||||
{},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "you must specify either header or details options",
|
||||
},
|
||||
{
|
||||
name: "no-ctx-eventer-and-syseventer-not-initialized",
|
||||
ctx: context.Background(),
|
||||
observationPayload: testPayloads,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing both context and system eventer",
|
||||
},
|
||||
{
|
||||
name: "use-syseventer",
|
||||
noFlush: true,
|
||||
ctx: context.Background(),
|
||||
observationPayload: []observationPayload{
|
||||
{
|
||||
header: map[string]interface{}{
|
||||
"name": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
header: map[string]interface{}{
|
||||
"name": "bar",
|
||||
},
|
||||
observationSinkFileName: c.AllEvents.Name(),
|
||||
setup: func() error {
|
||||
return event.InitSysEventer(hclog.Default(), c.EventerConfig)
|
||||
},
|
||||
cleanup: func() { event.TestResetSystEventer(t) },
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
ctx: testCtx,
|
||||
observationPayload: testPayloads,
|
||||
header: testWantHeader,
|
||||
details: testWantDetails,
|
||||
observationSinkFileName: c.AllEvents.Name(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
if tt.setup != nil {
|
||||
require.NoError(tt.setup())
|
||||
}
|
||||
if tt.cleanup != nil {
|
||||
defer tt.cleanup()
|
||||
}
|
||||
op := tt.name
|
||||
if tt.noOperation {
|
||||
op = ""
|
||||
}
|
||||
require.Greater(len(tt.observationPayload), 0)
|
||||
for _, p := range tt.observationPayload {
|
||||
err := event.WriteObservation(tt.ctx, event.Op(op), event.WithHeader(p.header), event.WithDetails(p.details))
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Errorf(err, "wanted error %q", tt.wantErrMatch)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
}
|
||||
if !tt.noFlush {
|
||||
require.NoError(event.WriteObservation(tt.ctx, event.Op(tt.name), event.WithFlush()))
|
||||
}
|
||||
|
||||
if tt.observationSinkFileName != "" {
|
||||
defer func() { _ = os.WriteFile(tt.observationSinkFileName, nil, 0o666) }()
|
||||
b, err := ioutil.ReadFile(tt.observationSinkFileName)
|
||||
assert.NoError(err)
|
||||
|
||||
gotObservation := &eventJson{}
|
||||
err = json.Unmarshal(b, gotObservation)
|
||||
require.NoErrorf(err, "json: %s", string(b))
|
||||
|
||||
actualJson, err := json.Marshal(gotObservation)
|
||||
require.NoError(err)
|
||||
wantJson := testObservationJsonFromCtx(t, tt.ctx, event.Op(tt.name), gotObservation, tt.header, tt.details)
|
||||
|
||||
assert.JSONEq(string(wantJson), string(actualJson))
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("not-enabled", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test",
|
||||
})
|
||||
|
||||
c := event.TestEventerConfig(t, "WriteObservation")
|
||||
c.EventerConfig.ObservationsEnabled = false
|
||||
|
||||
e, err := event.NewEventer(logger, c.EventerConfig)
|
||||
require.NoError(err)
|
||||
|
||||
testCtx, err := event.NewEventerContext(context.Background(), e)
|
||||
require.NoError(err)
|
||||
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
||||
require.NoError(err)
|
||||
|
||||
hdr := map[string]interface{}{
|
||||
"list": []string{"1", "2"},
|
||||
}
|
||||
require.NoError(event.WriteObservation(testCtx, "not-enabled", event.WithHeader(hdr), event.WithFlush()))
|
||||
|
||||
b, err := ioutil.ReadFile(c.AllEvents.Name())
|
||||
assert.NoError(err)
|
||||
assert.Len(b, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func testObservationJsonFromCtx(t *testing.T, ctx context.Context, caller event.Op, got *eventJson, hdr, details map[string]interface{}) []byte {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
reqInfo, _ := event.RequestInfoFromContext(ctx)
|
||||
// require.Truef(ok, "missing reqInfo in ctx")
|
||||
|
||||
j := eventJson{
|
||||
CreatedAt: got.CreatedAt,
|
||||
EventType: string(event.ObservationType),
|
||||
Payload: map[string]interface{}{
|
||||
event.IdField: got.Payload[event.IdField].(string),
|
||||
event.HeaderField: map[string]interface{}{
|
||||
event.RequestInfoField: reqInfo,
|
||||
event.VersionField: testObservationVersion,
|
||||
},
|
||||
},
|
||||
}
|
||||
if hdr != nil {
|
||||
h := j.Payload[event.HeaderField].(map[string]interface{})
|
||||
for k, v := range hdr {
|
||||
h[k] = v
|
||||
}
|
||||
}
|
||||
if details != nil {
|
||||
details[event.OpField] = string(caller)
|
||||
d := got.Payload[event.DetailsField].([]interface{})[0].(map[string]interface{})
|
||||
j.Payload[event.DetailsField] = []struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
Type string `json:"type"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
}{
|
||||
{
|
||||
CreatedAt: d[event.CreatedAtField].(string),
|
||||
Type: d[event.TypeField].(string),
|
||||
Payload: details,
|
||||
},
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(j)
|
||||
require.NoError(err)
|
||||
return b
|
||||
}
|
||||
|
||||
type eventJson struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
EventType string `json:"event_type"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
func Test_WriteAudit(t *testing.T) {
|
||||
// this test and its subtests cannot be run in parallel because of it's
|
||||
// dependency on the sysEventer
|
||||
now := time.Now()
|
||||
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test",
|
||||
})
|
||||
|
||||
c := event.TestEventerConfig(t, "WriteAudit")
|
||||
|
||||
e, err := event.NewEventer(logger, c.EventerConfig, event.WithNow(now))
|
||||
require.NoError(t, err)
|
||||
|
||||
info := &event.RequestInfo{Id: "867-5309"}
|
||||
|
||||
ctx, err := event.NewEventerContext(context.Background(), e)
|
||||
require.NoError(t, err)
|
||||
ctx, err = event.NewRequestInfoContext(ctx, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
testAuth := &event.Auth{}
|
||||
testReq := &event.Request{
|
||||
Operation: "POST",
|
||||
Endpoint: "/v1/hosts",
|
||||
Details: &pbs.CreateHostRequest{Item: &pb.Host{
|
||||
HostCatalogId: "hc_1234567890",
|
||||
Name: &wrappers.StringValue{Value: "name"},
|
||||
Description: &wrappers.StringValue{Value: "desc"},
|
||||
Type: "static",
|
||||
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
|
||||
"address": structpb.NewStringValue("123.456.789"),
|
||||
}},
|
||||
}},
|
||||
}
|
||||
testAuthorizedActions := []string{"no-op", "read", "update", "delete"}
|
||||
|
||||
testResp := &event.Response{
|
||||
StatusCode: 200,
|
||||
Details: &pbs.CreateHostResponse{
|
||||
Uri: fmt.Sprintf("hosts/%s_", static.HostPrefix),
|
||||
Item: &pb.Host{
|
||||
HostCatalogId: "hc_1234567890",
|
||||
Scope: &scopes.ScopeInfo{Id: "proj_1234567890", Type: scope.Project.String(), ParentScopeId: "org_1234567890"},
|
||||
Name: &wrappers.StringValue{Value: "name"},
|
||||
Description: &wrappers.StringValue{Value: "desc"},
|
||||
Type: "static",
|
||||
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
|
||||
"address": structpb.NewStringValue("123.456.789"),
|
||||
}},
|
||||
AuthorizedActions: testAuthorizedActions,
|
||||
},
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
auditOpts [][]event.Option
|
||||
wantAudit *testAudit
|
||||
ctx context.Context
|
||||
auditSinkFileName string
|
||||
setup func() error
|
||||
cleanup func()
|
||||
noOperation bool
|
||||
noFlush bool
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-ctx",
|
||||
auditOpts: [][]event.Option{
|
||||
{
|
||||
event.WithAuth(testAuth),
|
||||
event.WithRequest(testReq),
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing context",
|
||||
},
|
||||
{
|
||||
name: "missing-op",
|
||||
ctx: ctx,
|
||||
auditOpts: [][]event.Option{
|
||||
{
|
||||
event.WithAuth(testAuth),
|
||||
event.WithRequest(testReq),
|
||||
},
|
||||
},
|
||||
noOperation: true,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing operation",
|
||||
},
|
||||
{
|
||||
name: "no-ctx-eventer-and-syseventer-not-initialized",
|
||||
ctx: context.Background(),
|
||||
auditOpts: [][]event.Option{
|
||||
{
|
||||
event.WithAuth(testAuth),
|
||||
event.WithRequest(testReq),
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing both context and system eventer",
|
||||
},
|
||||
{
|
||||
name: "use-syseventer",
|
||||
noFlush: true,
|
||||
ctx: context.Background(),
|
||||
auditOpts: [][]event.Option{
|
||||
{
|
||||
event.WithAuth(testAuth),
|
||||
event.WithRequest(testReq),
|
||||
},
|
||||
},
|
||||
wantAudit: &testAudit{
|
||||
Auth: testAuth,
|
||||
Request: testReq,
|
||||
},
|
||||
setup: func() error {
|
||||
return event.InitSysEventer(hclog.Default(), c.EventerConfig)
|
||||
},
|
||||
cleanup: func() { event.TestResetSystEventer(t) },
|
||||
auditSinkFileName: c.AllEvents.Name(),
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
ctx: ctx,
|
||||
auditOpts: [][]event.Option{
|
||||
{
|
||||
event.WithAuth(testAuth),
|
||||
event.WithRequest(testReq),
|
||||
},
|
||||
{
|
||||
event.WithResponse(testResp),
|
||||
},
|
||||
},
|
||||
wantAudit: &testAudit{
|
||||
Id: "867-5309",
|
||||
Auth: testAuth,
|
||||
Request: testReq,
|
||||
Response: testResp,
|
||||
},
|
||||
auditSinkFileName: c.AllEvents.Name(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
if tt.setup != nil {
|
||||
require.NoError(tt.setup())
|
||||
}
|
||||
if tt.cleanup != nil {
|
||||
defer tt.cleanup()
|
||||
}
|
||||
op := tt.name
|
||||
if tt.noOperation {
|
||||
op = ""
|
||||
}
|
||||
require.Greater(len(tt.auditOpts), 0)
|
||||
for _, opts := range tt.auditOpts {
|
||||
opts := append(opts, event.WithNow(now))
|
||||
err := event.WriteAudit(tt.ctx, event.Op(op), opts...)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Errorf(err, "wanted error %q", tt.wantErrMatch)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
}
|
||||
if !tt.noFlush {
|
||||
require.NoError(event.WriteAudit(tt.ctx, event.Op(op), event.WithFlush(), event.WithNow(now)))
|
||||
}
|
||||
if tt.auditSinkFileName != "" {
|
||||
defer func() { _ = os.WriteFile(tt.auditSinkFileName, nil, 0o666) }()
|
||||
|
||||
b, err := ioutil.ReadFile(tt.auditSinkFileName)
|
||||
require.NoError(err)
|
||||
gotAudit := &eventJson{}
|
||||
err = json.Unmarshal(b, gotAudit)
|
||||
require.NoErrorf(err, "json: %s", string(b))
|
||||
|
||||
actualJson, err := json.Marshal(gotAudit)
|
||||
require.NoError(err)
|
||||
|
||||
wantEvent := eventJson{
|
||||
CreatedAt: gotAudit.CreatedAt,
|
||||
EventType: string(gotAudit.EventType),
|
||||
Payload: map[string]interface{}{
|
||||
"auth": tt.wantAudit.Auth,
|
||||
"id": gotAudit.Payload["id"],
|
||||
"timestamp": now,
|
||||
"request": tt.wantAudit.Request,
|
||||
"serialized_hmac": "",
|
||||
"type": apiRequest,
|
||||
"version": testAuditVersion,
|
||||
},
|
||||
}
|
||||
if tt.wantAudit.Id != "" {
|
||||
wantEvent.Payload["id"] = tt.wantAudit.Id
|
||||
wantEvent.Payload["request_info"] = event.RequestInfo{
|
||||
Id: tt.wantAudit.Id,
|
||||
}
|
||||
}
|
||||
if tt.wantAudit.Response != nil {
|
||||
wantEvent.Payload["response"] = tt.wantAudit.Response
|
||||
}
|
||||
wantJson, err := json.Marshal(wantEvent)
|
||||
require.NoError(err)
|
||||
|
||||
assert.JSONEq(string(wantJson), string(actualJson))
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("not-enabled", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test",
|
||||
})
|
||||
|
||||
c := event.TestEventerConfig(t, "WriteAudit")
|
||||
c.EventerConfig.AuditEnabled = false
|
||||
|
||||
e, err := event.NewEventer(logger, c.EventerConfig)
|
||||
require.NoError(err)
|
||||
|
||||
testCtx, err := event.NewEventerContext(context.Background(), e)
|
||||
require.NoError(err)
|
||||
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
||||
require.NoError(err)
|
||||
|
||||
require.NoError(event.WriteAudit(testCtx, "not-enabled", event.WithRequest(testReq), event.WithFlush()))
|
||||
b, err := ioutil.ReadFile(c.AllEvents.Name())
|
||||
assert.NoError(err)
|
||||
assert.Len(b, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_WriteError(t *testing.T) {
|
||||
// this test and its subtests cannot be run in parallel because of it's
|
||||
// dependency on the sysEventer
|
||||
now := time.Now()
|
||||
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test",
|
||||
})
|
||||
|
||||
c := event.TestEventerConfig(t, "WriteAudit")
|
||||
|
||||
e, err := event.NewEventer(logger, c.EventerConfig, event.WithNow(now))
|
||||
require.NoError(t, err)
|
||||
|
||||
info := &event.RequestInfo{Id: "867-5309"}
|
||||
|
||||
testCtx, err := event.NewEventerContext(context.Background(), e)
|
||||
require.NoError(t, err)
|
||||
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCtxNoInfoId, err := event.NewEventerContext(context.Background(), e)
|
||||
require.NoError(t, err)
|
||||
noId := &event.RequestInfo{Id: "867-5309"}
|
||||
testCtxNoInfoId, err = event.NewRequestInfoContext(testCtxNoInfoId, noId)
|
||||
require.NoError(t, err)
|
||||
noId.Id = ""
|
||||
|
||||
testError := errors.New(errors.InvalidParameter, "test", "you lookin at me?")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
e error
|
||||
info *event.RequestInfo
|
||||
setup func() error
|
||||
cleanup func()
|
||||
noOperation bool
|
||||
errSinkFileName string
|
||||
noOutput bool
|
||||
}{
|
||||
{
|
||||
name: "missing-caller",
|
||||
ctx: testCtx,
|
||||
e: testError,
|
||||
noOperation: true,
|
||||
noOutput: true,
|
||||
},
|
||||
{
|
||||
name: "no-ctx-eventer-and-syseventer-not-initialized",
|
||||
ctx: context.Background(),
|
||||
e: testError,
|
||||
errSinkFileName: c.ErrorEvents.Name(),
|
||||
noOutput: true,
|
||||
},
|
||||
{
|
||||
name: "use-syseventer",
|
||||
ctx: context.Background(),
|
||||
e: testError,
|
||||
setup: func() error {
|
||||
return event.InitSysEventer(hclog.Default(), c.EventerConfig)
|
||||
},
|
||||
cleanup: func() { event.TestResetSystEventer(t) },
|
||||
errSinkFileName: c.ErrorEvents.Name(),
|
||||
},
|
||||
{
|
||||
name: "no-info-id",
|
||||
ctx: testCtxNoInfoId,
|
||||
e: testError,
|
||||
info: &event.RequestInfo{},
|
||||
setup: func() error {
|
||||
return event.InitSysEventer(hclog.Default(), c.EventerConfig)
|
||||
},
|
||||
cleanup: func() { event.TestResetSystEventer(t) },
|
||||
errSinkFileName: c.ErrorEvents.Name(),
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
ctx: testCtx,
|
||||
e: testError,
|
||||
info: info,
|
||||
errSinkFileName: c.ErrorEvents.Name(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
if tt.setup != nil {
|
||||
require.NoError(tt.setup())
|
||||
}
|
||||
if tt.cleanup != nil {
|
||||
defer tt.cleanup()
|
||||
}
|
||||
op := tt.name
|
||||
if tt.noOperation {
|
||||
op = ""
|
||||
}
|
||||
event.WriteError(tt.ctx, event.Op(op), tt.e)
|
||||
if tt.errSinkFileName != "" {
|
||||
defer func() { _ = os.WriteFile(tt.errSinkFileName, nil, 0666) }()
|
||||
b, err := ioutil.ReadFile(tt.errSinkFileName)
|
||||
require.NoError(err)
|
||||
|
||||
if tt.noOutput {
|
||||
assert.Lenf(b, 0, "should be an empty file: %s", string(b))
|
||||
return
|
||||
}
|
||||
|
||||
gotError := &eventJson{}
|
||||
err = json.Unmarshal(b, gotError)
|
||||
require.NoErrorf(err, "json: %s", string(b))
|
||||
|
||||
actualJson, err := json.Marshal(gotError)
|
||||
require.NoError(err)
|
||||
|
||||
wantError := eventJson{
|
||||
CreatedAt: gotError.CreatedAt,
|
||||
EventType: string(gotError.EventType),
|
||||
Payload: map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
"Code": tt.e.(*errors.Err).Code,
|
||||
"Msg": tt.e.(*errors.Err).Msg,
|
||||
"Op": tt.e.(*errors.Err).Op,
|
||||
"Wrapped": tt.e.(*errors.Err).Wrapped,
|
||||
},
|
||||
"id": gotError.Payload["id"],
|
||||
"op": op,
|
||||
"version": testErrorVersion,
|
||||
},
|
||||
}
|
||||
if tt.info != nil && tt.info.Id != "" {
|
||||
wantError.Payload["id"] = tt.info.Id
|
||||
}
|
||||
if tt.info != nil {
|
||||
wantError.Payload["request_info"] = tt.info
|
||||
|
||||
}
|
||||
wantJson, err := json.Marshal(wantError)
|
||||
require.NoError(err)
|
||||
|
||||
assert.JSONEq(string(wantJson), string(actualJson))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package event
|
||||
|
||||
import "google.golang.org/protobuf/proto"
|
||||
|
||||
type (
|
||||
Id string
|
||||
Op string
|
||||
)
|
||||
|
||||
// RequestInfo defines the fields captured about a Boundary request.
|
||||
type RequestInfo struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
PublicId string `json:"public_id,omitempty"`
|
||||
}
|
||||
|
||||
// UserInfo defines the fields captured about a user for a Boundary request.
|
||||
type UserInfo struct {
|
||||
UserId string `json:"id,omitempty"`
|
||||
AuthAccountId string `json:"auth_account_id,omitempty"`
|
||||
}
|
||||
|
||||
type GrantsInfo struct {
|
||||
Grants []GrantsPair `json:"grants_pair,omitempty"`
|
||||
}
|
||||
|
||||
type GrantsPair struct {
|
||||
Grant string `json:"grant,omitempty"`
|
||||
ScopeId string `json:"scope_id,omitempty"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
// AccessorId is a std audit field == auth_token public_id
|
||||
AccessorId string `json:"accessor_id"`
|
||||
UserInfo *UserInfo `json:"user_info,omitempty"` // boundary field
|
||||
GrantsInfo *GrantsInfo `json:"grants_info,omitempty"`
|
||||
UserEmail string `json:"email,omitempty"`
|
||||
UserName string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Operation string `json:"operation"` // std audit field
|
||||
Endpoint string `json:"endpoint"` // std audit field
|
||||
Details proto.Message `json:"details"` // boundary field
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
StatusCode int `json:"status_code,omitempty"` // std audit
|
||||
Details proto.Message `json:"details,omitempty"` // boundary field
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
)
|
||||
|
||||
// auditVersion defines the version of audit events
|
||||
const auditVersion = "v0.1"
|
||||
|
||||
// auditEventType defines the type of audit event
|
||||
type auditEventType string
|
||||
|
||||
const (
|
||||
apiRequest auditEventType = "APIRequest" // ApiRequest defines an API request audit event type
|
||||
)
|
||||
|
||||
// audit defines the data of audit events
|
||||
type audit struct {
|
||||
Id string `json:"id"` // std audit/boundary field
|
||||
Version string `json:"version"` // std audit/boundary field
|
||||
Type string `json:"type"` // std audit field
|
||||
Timestamp time.Time `json:"timestamp"` // std audit field
|
||||
RequestInfo *RequestInfo `json:"request_info,omitempty"` // boundary field
|
||||
Auth *Auth `json:"auth,omitempty"` // std audit field
|
||||
Request *Request `json:"request,omitempty"` // std audit field
|
||||
Response *Response `json:"response,omitempty"` // std audit field
|
||||
SerializedHMAC string `json:"serialized_hmac"` // boundary field
|
||||
Flush bool `json:"-"`
|
||||
}
|
||||
|
||||
func newAudit(fromOperation Op, opt ...Option) (*audit, error) {
|
||||
const op = "event.newAudit"
|
||||
if fromOperation == "" {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing from operation")
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withId == "" {
|
||||
var err error
|
||||
opts.withId, err = newId(string(AuditType))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
}
|
||||
var dtm time.Time
|
||||
switch opts.withNow.IsZero() {
|
||||
case false:
|
||||
dtm = opts.withNow
|
||||
default:
|
||||
dtm = time.Now()
|
||||
}
|
||||
|
||||
a := &audit{
|
||||
Id: opts.withId,
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
Timestamp: dtm,
|
||||
RequestInfo: opts.withRequestInfo,
|
||||
Auth: opts.withAuth,
|
||||
Request: opts.withRequest,
|
||||
Response: opts.withResponse,
|
||||
Flush: opts.withFlush,
|
||||
}
|
||||
if err := a.validate(); err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// EventType is required for all event types by the eventlogger broker
|
||||
func (a *audit) EventType() string { return string(AuditType) }
|
||||
|
||||
func (a *audit) validate() error {
|
||||
const op = "event.(audit).validate"
|
||||
if a.Id == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing id")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetID is part of the eventlogger.Gateable interface and returns the audit
|
||||
// event's id.
|
||||
func (a *audit) GetID() string {
|
||||
return a.Id
|
||||
}
|
||||
|
||||
// FlushEvent is part of the eventlogger.Gateable interface and returns the
|
||||
// value of the audit event's flush field
|
||||
func (a *audit) FlushEvent() bool {
|
||||
return a.Flush
|
||||
}
|
||||
|
||||
// ComposedFrom is part of the eventlogger.Gatable interface. It's important to
|
||||
// remember that the receiver will always be nil when this is called by the eventlogger.GatedFilter
|
||||
func (a *audit) ComposeFrom(events []*eventlogger.Event) (eventlogger.EventType, interface{}, error) {
|
||||
const op = "event.(audit).ComposedFrom"
|
||||
if len(events) == 0 {
|
||||
return "", nil, errors.New(errors.InvalidParameter, op, "missing events")
|
||||
}
|
||||
var validId string
|
||||
payload := audit{}
|
||||
for i, v := range events {
|
||||
gated, ok := v.Payload.(*audit)
|
||||
if !ok {
|
||||
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d is not an audit payload", i))
|
||||
}
|
||||
if gated.Id == "" {
|
||||
// can't really happen since it has to have an id to be gated, but
|
||||
// I'll add this check in the name of completeness
|
||||
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d: id is required", i))
|
||||
}
|
||||
if validId == "" {
|
||||
validId = gated.Id
|
||||
}
|
||||
if gated.Id != validId {
|
||||
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d has an invalid id: %s != %s", i, gated.Id, validId))
|
||||
}
|
||||
if gated.Version != auditVersion {
|
||||
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d has an invalid version: %s != %s", i, gated.Version, auditVersion))
|
||||
}
|
||||
if gated.Type != string(apiRequest) {
|
||||
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d has an invalid type: %s != %s", i, gated.Type, string(AuditType)))
|
||||
}
|
||||
if gated.RequestInfo != nil {
|
||||
payload.RequestInfo = gated.RequestInfo
|
||||
}
|
||||
if gated.Auth != nil {
|
||||
payload.Auth = gated.Auth
|
||||
}
|
||||
if gated.Request != nil {
|
||||
payload.Request = gated.Request
|
||||
}
|
||||
if gated.Response != nil {
|
||||
payload.Response = gated.Response
|
||||
}
|
||||
if !gated.Timestamp.IsZero() {
|
||||
payload.Timestamp = gated.Timestamp
|
||||
}
|
||||
|
||||
}
|
||||
payload.Id = validId
|
||||
payload.Version = auditVersion
|
||||
payload.Type = string(apiRequest)
|
||||
return eventlogger.EventType(a.EventType()), payload, nil
|
||||
}
|
||||
@ -0,0 +1,278 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newAudit(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNow := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fromOp Op
|
||||
opts []Option
|
||||
want *audit
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
{
|
||||
name: "missing-op",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "valid-no-opts",
|
||||
fromOp: "valid-no-opts",
|
||||
want: &audit{
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all-opts",
|
||||
fromOp: "all-opts",
|
||||
opts: []Option{
|
||||
WithId("all-opts"),
|
||||
WithNow(testNow),
|
||||
WithRequestInfo(TestRequestInfo(t)),
|
||||
WithAuth(testAuth(t)),
|
||||
WithRequest(testRequest(t)),
|
||||
WithResponse(testResponse(t)),
|
||||
WithFlush(),
|
||||
},
|
||||
want: &audit{
|
||||
Id: "all-opts",
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
Timestamp: testNow,
|
||||
RequestInfo: TestRequestInfo(t),
|
||||
Auth: testAuth(t),
|
||||
Request: testRequest(t),
|
||||
Response: testResponse(t),
|
||||
Flush: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := newAudit(tt.fromOp, tt.opts...)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
opts := getOpts(tt.opts...)
|
||||
if opts.withId == "" {
|
||||
tt.want.Id = got.Id
|
||||
}
|
||||
if opts.withNow.IsZero() {
|
||||
tt.want.Timestamp = got.Timestamp
|
||||
}
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAudit_validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-id",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing id",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
id: "valid",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
a := audit{Id: tt.id}
|
||||
err := a.validate()
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAudit_EventType(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := &audit{}
|
||||
assert.Equal(t, string(AuditType), a.EventType())
|
||||
}
|
||||
|
||||
func TestAudit_GetID(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := &audit{Id: "test"}
|
||||
assert.Equal(t, "test", a.GetID())
|
||||
}
|
||||
|
||||
func TestAudit_FlushEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := &audit{Flush: true}
|
||||
assert.True(t, a.FlushEvent())
|
||||
a.Flush = false
|
||||
assert.False(t, a.FlushEvent())
|
||||
}
|
||||
|
||||
func TestAudit_ComposeFrom(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNow := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
events []*eventlogger.Event
|
||||
want audit
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-events",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing events",
|
||||
},
|
||||
{
|
||||
name: "not-an-audit",
|
||||
events: []*eventlogger.Event{{
|
||||
Payload: struct{}{},
|
||||
}},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not an audit payload",
|
||||
},
|
||||
{
|
||||
name: "invalid-type",
|
||||
events: []*eventlogger.Event{
|
||||
{
|
||||
Payload: &audit{
|
||||
Id: "test-id",
|
||||
Version: auditVersion,
|
||||
Type: "invalid-type",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "invalid type",
|
||||
},
|
||||
{
|
||||
name: "invalid-version",
|
||||
events: []*eventlogger.Event{
|
||||
{
|
||||
Payload: &audit{
|
||||
Id: "test-id",
|
||||
Version: "invalid-version",
|
||||
Type: string(apiRequest),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "invalid version",
|
||||
},
|
||||
{
|
||||
name: "invalid-id",
|
||||
events: []*eventlogger.Event{
|
||||
{
|
||||
Payload: &audit{
|
||||
Id: "invalid-id",
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
},
|
||||
},
|
||||
{
|
||||
Payload: &audit{
|
||||
Id: "bad-id",
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "invalid id",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
events: []*eventlogger.Event{
|
||||
{
|
||||
Payload: &audit{
|
||||
Id: "valid",
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
Timestamp: testNow,
|
||||
Auth: testAuth(t),
|
||||
RequestInfo: TestRequestInfo(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
Payload: &audit{
|
||||
Id: "valid",
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
Timestamp: testNow,
|
||||
Request: testRequest(t),
|
||||
},
|
||||
},
|
||||
{
|
||||
Payload: &audit{
|
||||
Id: "valid",
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
Timestamp: testNow,
|
||||
Response: testResponse(t),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: audit{
|
||||
Id: "valid",
|
||||
Version: auditVersion,
|
||||
Type: string(apiRequest),
|
||||
Timestamp: testNow,
|
||||
Auth: testAuth(t),
|
||||
Request: testRequest(t),
|
||||
Response: testResponse(t),
|
||||
RequestInfo: TestRequestInfo(t),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
a := &audit{}
|
||||
gotType, gotAudit, err := a.ComposeFrom(tt.events)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(gotAudit)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(gotAudit)
|
||||
assert.Equal(eventlogger.EventType(a.EventType()), gotType)
|
||||
tt.want.Timestamp = gotAudit.(audit).Timestamp
|
||||
assert.Equal(tt.want, gotAudit)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDeliveryGuarantee DeliveryGuarantee = "" // DefaultDeliveryGuarantee will be BestEffort
|
||||
Enforced DeliveryGuarantee = "enforced" // Enforced means that a delivery guarantee is enforced
|
||||
BestEffort DeliveryGuarantee = "best-effort" // BestEffort means that a best effort will be made to deliver an event
|
||||
)
|
||||
|
||||
type DeliveryGuarantee string // DeliveryGuarantee defines the guarantees around delivery of an event type within config
|
||||
|
||||
func (g DeliveryGuarantee) validate() error {
|
||||
const op = "event.(DeliveryGuarantee).validate"
|
||||
switch g {
|
||||
case DefaultDeliveryGuarantee, BestEffort, Enforced:
|
||||
return nil
|
||||
default:
|
||||
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s is not a valid delivery guarantee", g))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDeliveryGuarantee_validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
g DeliveryGuarantee
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "invalid",
|
||||
g: "invalid",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not a valid delivery guarantee",
|
||||
},
|
||||
{
|
||||
name: "BestEffort",
|
||||
g: BestEffort,
|
||||
},
|
||||
{
|
||||
name: "Default",
|
||||
g: DefaultDeliveryGuarantee,
|
||||
},
|
||||
{
|
||||
name: "Enforced",
|
||||
g: Enforced,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
err := tt.g.validate()
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package event
|
||||
|
||||
import "github.com/hashicorp/boundary/internal/errors"
|
||||
|
||||
// errorVersion defines the version of error events
|
||||
const errorVersion = "v0.1"
|
||||
|
||||
type err struct {
|
||||
Error error `json:"error"`
|
||||
Id Id `json:"id,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Op Op `json:"op,omitempty"`
|
||||
RequestInfo *RequestInfo `json:"request_info,omitempty"`
|
||||
}
|
||||
|
||||
func newError(fromOperation Op, e error, opt ...Option) (*err, error) {
|
||||
const op = "event.newError"
|
||||
if fromOperation == "" {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing operation")
|
||||
}
|
||||
if e == nil {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing error")
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withId == "" {
|
||||
var err error
|
||||
opts.withId, err = newId(string(ErrorType))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
}
|
||||
newErr := &err{
|
||||
Id: Id(opts.withId),
|
||||
Op: fromOperation,
|
||||
Version: errorVersion,
|
||||
RequestInfo: opts.withRequestInfo,
|
||||
Error: e,
|
||||
}
|
||||
if err := newErr.validate(); err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
return newErr, nil
|
||||
}
|
||||
|
||||
// EventType is required for all event types by the eventlogger broker
|
||||
func (e *err) EventType() string { return string(ErrorType) }
|
||||
|
||||
func (e *err) validate() error {
|
||||
const op = "event.(err).validate"
|
||||
if e.Id == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing id")
|
||||
}
|
||||
if e.Op == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing operation")
|
||||
}
|
||||
if e.Error == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fromOp Op
|
||||
e error
|
||||
opts []Option
|
||||
want *err
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-op",
|
||||
e: errors.New(errors.InvalidParameter, "missing-operation", "missing operation"),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing operation",
|
||||
},
|
||||
{
|
||||
name: "missing-error",
|
||||
fromOp: Op("missing-error"),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing error",
|
||||
},
|
||||
{
|
||||
name: "valid-no-opts",
|
||||
fromOp: Op("valid-no-opts"),
|
||||
e: errors.New(errors.InvalidParameter, "valid-no-opts", "valid no opts"),
|
||||
want: &err{
|
||||
Error: errors.New(errors.InvalidParameter, "valid-no-opts", "valid no opts"),
|
||||
Version: errorVersion,
|
||||
Op: Op("valid-no-opts"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-all-opts",
|
||||
fromOp: Op("valid-all-opts"),
|
||||
e: errors.New(errors.InvalidParameter, "valid-all-opts", "valid all opts"),
|
||||
opts: []Option{
|
||||
WithId("valid-all-opts"),
|
||||
WithRequestInfo(TestRequestInfo(t)),
|
||||
},
|
||||
want: &err{
|
||||
Error: errors.New(errors.InvalidParameter, "valid-all-opts", "valid all opts"),
|
||||
Version: errorVersion,
|
||||
Op: Op("valid-all-opts"),
|
||||
Id: "valid-all-opts",
|
||||
RequestInfo: TestRequestInfo(t),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := newError(tt.fromOp, tt.e, tt.opts...)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
opts := getOpts(tt.opts...)
|
||||
if opts.withId == "" {
|
||||
tt.want.Id = got.Id
|
||||
}
|
||||
if opts.withRequestInfo != nil {
|
||||
tt.want.RequestInfo = got.RequestInfo
|
||||
}
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_errvalidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
op Op
|
||||
want error
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-id",
|
||||
op: Op("missing-id"),
|
||||
want: errors.New(errors.InvalidParameter, "missing-id", "missing id"),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing id",
|
||||
},
|
||||
{
|
||||
name: "missing-operation",
|
||||
id: "missing-operation",
|
||||
want: errors.New(errors.InvalidParameter, "missing-operation", "missing operation"),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing operation",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
op: Op("valid"),
|
||||
id: "valid",
|
||||
want: errors.New(errors.InvalidParameter, "valid", "valid error"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
e := err{
|
||||
Op: tt.op,
|
||||
Id: Id(tt.id),
|
||||
Error: tt.want,
|
||||
}
|
||||
err := e.validate()
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tt.want, e.Error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_errEventType(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := &err{}
|
||||
assert.Equal(t, string(ErrorType), e.EventType())
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/vault/sdk/helper/strutil"
|
||||
)
|
||||
|
||||
// observationVersion defines the version of observation events
|
||||
const observationVersion = "v0.1"
|
||||
|
||||
type observation struct {
|
||||
*eventlogger.SimpleGatedPayload
|
||||
Version string `json:"version"`
|
||||
Op Op `json:"op,omitempty"`
|
||||
RequestInfo *RequestInfo `json:"request_info,omitempty"`
|
||||
}
|
||||
|
||||
func newObservation(fromOperation Op, opt ...Option) (*observation, error) {
|
||||
const op = "event.newObservation"
|
||||
if fromOperation == "" {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing operation")
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withId == "" {
|
||||
var err error
|
||||
opts.withId, err = newId(string(ObservationType))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
}
|
||||
for k := range opts.withHeader {
|
||||
if strutil.StrListContains([]string{OpField, VersionField, RequestInfoField}, k) {
|
||||
return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s is a reserved field name", k))
|
||||
}
|
||||
}
|
||||
i := &observation{
|
||||
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{
|
||||
ID: opts.withId,
|
||||
Header: opts.withHeader,
|
||||
Detail: opts.withDetails,
|
||||
Flush: opts.withFlush,
|
||||
},
|
||||
Op: fromOperation,
|
||||
RequestInfo: opts.withRequestInfo,
|
||||
Version: observationVersion,
|
||||
}
|
||||
if err := i.validate(); err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// EventType is required for all event types by the eventlogger broker
|
||||
func (i *observation) EventType() string { return string(ObservationType) }
|
||||
|
||||
func (i *observation) validate() error {
|
||||
const op = "event.(Observation).validate"
|
||||
if i.ID == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing id")
|
||||
}
|
||||
if i.Op == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing operation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newObservation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
testHeader := map[string]interface{}{
|
||||
"public-id": "public-id",
|
||||
"now": now,
|
||||
}
|
||||
|
||||
testDetails := map[string]interface{}{
|
||||
"file_name": "tmpfile-name",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fromOp Op
|
||||
opts []Option
|
||||
want *observation
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-op",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing operation",
|
||||
},
|
||||
{
|
||||
name: "valid-no-opts",
|
||||
fromOp: Op("valid-no-opts"),
|
||||
want: &observation{
|
||||
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{},
|
||||
Version: errorVersion,
|
||||
Op: Op("valid-no-opts"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-all-opts",
|
||||
fromOp: Op("valid-all-opts"),
|
||||
opts: []Option{
|
||||
WithId("valid-all-opts"),
|
||||
WithRequestInfo(TestRequestInfo(t)),
|
||||
WithHeader(testHeader),
|
||||
WithDetails(testDetails),
|
||||
WithFlush(),
|
||||
},
|
||||
want: &observation{
|
||||
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{
|
||||
ID: "valid-all-opts",
|
||||
Header: testHeader,
|
||||
Detail: testDetails,
|
||||
Flush: true,
|
||||
},
|
||||
Version: errorVersion,
|
||||
Op: Op("valid-all-opts"),
|
||||
RequestInfo: TestRequestInfo(t),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := newObservation(tt.fromOp, tt.opts...)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
opts := getOpts(tt.opts...)
|
||||
if opts.withId == "" {
|
||||
tt.want.ID = got.ID
|
||||
}
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_observationvalidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
op Op
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-id",
|
||||
op: Op("missing-id"),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing id",
|
||||
},
|
||||
{
|
||||
name: "missing-operation",
|
||||
id: "missing-operation",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing operation",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
op: Op("valid"),
|
||||
id: "valid",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
e := observation{
|
||||
Op: tt.op,
|
||||
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{
|
||||
ID: tt.id,
|
||||
},
|
||||
}
|
||||
err := e.validate()
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_observationEventType(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := &observation{}
|
||||
assert.Equal(t, string(ObservationType), e.EventType())
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
// Type represents the event's type
|
||||
type Type string
|
||||
|
||||
const (
|
||||
EveryType Type = "*" // EveryType represents every (all) types of events
|
||||
ObservationType Type = "observation" // ObservationType represents observation events
|
||||
AuditType Type = "audit" // AuditType represents audit events
|
||||
ErrorType Type = "error" // ErrorType represents error events
|
||||
)
|
||||
|
||||
func (et Type) validate() error {
|
||||
const op = "event.(Type).validate"
|
||||
switch et {
|
||||
case EveryType, ObservationType, AuditType, ErrorType:
|
||||
return nil
|
||||
default:
|
||||
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("'%s' is not a valid event type", et))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,398 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
const (
|
||||
OpField = "op" // OpField in an event.
|
||||
RequestInfoField = "request_info" // RequestInfoField in an event.
|
||||
VersionField = "version" // VersionField in an event
|
||||
DetailsField = "details" // Details field in an event.
|
||||
HeaderField = "header" // HeaderField in an event.
|
||||
IdField = "id" // IdField in an event.
|
||||
CreatedAtField = "created_at" // CreatedAtField in an event.
|
||||
TypeField = "type" // TypeField in an event.
|
||||
|
||||
auditPipeline = "audit-pipeline" // auditPipeline is a pipeline for audit events
|
||||
observationPipeline = "observation-pipeline" // observationPipeline is a pipeline for observation events
|
||||
errPipeline = "err-pipeline" // errPipeline is a pipeline for error events
|
||||
)
|
||||
|
||||
// flushable defines an interface that all eventlogger Nodes must implement if
|
||||
// they are "flushable"
|
||||
type flushable interface {
|
||||
FlushAll(ctx context.Context) error
|
||||
}
|
||||
|
||||
// broker defines an interface for an eventlogger Broker... which will allow us
|
||||
// to substitute our testing broker when needed to write tests for things
|
||||
// like event send retrying.
|
||||
type broker interface {
|
||||
Send(ctx context.Context, t eventlogger.EventType, payload interface{}) (eventlogger.Status, error)
|
||||
Reopen(ctx context.Context) error
|
||||
StopTimeAt(now time.Time)
|
||||
RegisterNode(id eventlogger.NodeID, node eventlogger.Node) error
|
||||
SetSuccessThreshold(t eventlogger.EventType, successThreshold int) error
|
||||
RegisterPipeline(def eventlogger.Pipeline) error
|
||||
}
|
||||
|
||||
// Eventer provides a method to send events to pipelines of sinks
|
||||
type Eventer struct {
|
||||
broker broker
|
||||
flushableNodes []flushable
|
||||
conf EventerConfig
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
sysEventer *Eventer // sysEventer is the system-wide Eventer
|
||||
sysEventerOnce sync.Once // sysEventerOnce ensures that the system-wide Eventer is only initialized once.
|
||||
)
|
||||
|
||||
// InitSysEventer provides a mechanism to initialize a "system wide" eventer
|
||||
// singleton for Boundary
|
||||
func InitSysEventer(log hclog.Logger, c EventerConfig) error {
|
||||
const op = "event.InitSysEventer"
|
||||
if log == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing hclog")
|
||||
}
|
||||
// the order of operations is important here. we want to determine if
|
||||
// there's an error before setting the singleton sysEventer which can only
|
||||
// be done one time.
|
||||
eventer, err := NewEventer(log, c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
sysEventerOnce.Do(func() {
|
||||
sysEventer = eventer
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SysEventer returns the "system wide" eventer for Boundary.
|
||||
func SysEventer() *Eventer {
|
||||
return sysEventer
|
||||
}
|
||||
|
||||
// NewEventer creates a new Eventer using the config. Supports options: WithNow
|
||||
func NewEventer(log hclog.Logger, c EventerConfig, opt ...Option) (*Eventer, error) {
|
||||
const op = "event.NewEventer"
|
||||
if log == nil {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "missing logger")
|
||||
}
|
||||
|
||||
// if there are no sinks in config, then we'll default to just one stdout
|
||||
// sink.
|
||||
if len(c.Sinks) == 0 {
|
||||
c.Sinks = append(c.Sinks, SinkConfig{
|
||||
Name: "default",
|
||||
EventTypes: []Type{EveryType},
|
||||
Format: JSONSinkFormat,
|
||||
SinkType: StdoutSink,
|
||||
})
|
||||
}
|
||||
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
|
||||
type pipeline struct {
|
||||
eventType Type
|
||||
fmtId eventlogger.NodeID
|
||||
sinkId eventlogger.NodeID
|
||||
}
|
||||
var auditPipelines, observationPipelines, errPipelines []pipeline
|
||||
|
||||
opts := getOpts(opt...)
|
||||
var b broker
|
||||
switch {
|
||||
case opts.withBroker != nil:
|
||||
b = opts.withBroker
|
||||
default:
|
||||
b = eventlogger.NewBroker()
|
||||
}
|
||||
|
||||
e := &Eventer{
|
||||
logger: log,
|
||||
conf: c,
|
||||
broker: b,
|
||||
}
|
||||
|
||||
if !opts.withNow.IsZero() {
|
||||
e.broker.StopTimeAt(opts.withNow)
|
||||
}
|
||||
|
||||
// Create JSONFormatter node
|
||||
id, err := newId("json")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
jsonfmtId := eventlogger.NodeID(id)
|
||||
fmtNode := &eventlogger.JSONFormatter{}
|
||||
err = e.broker.RegisterNode(jsonfmtId, fmtNode)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to register json node")
|
||||
}
|
||||
|
||||
for _, s := range c.Sinks {
|
||||
var sinkId eventlogger.NodeID
|
||||
var sinkNode eventlogger.Node
|
||||
switch s.SinkType {
|
||||
case StdoutSink:
|
||||
sinkNode = &eventlogger.WriterSink{
|
||||
Format: string(s.Format),
|
||||
Writer: os.Stdout,
|
||||
}
|
||||
id, err = newId("stdout")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
sinkId = eventlogger.NodeID(id)
|
||||
default:
|
||||
sinkNode = &eventlogger.FileSink{
|
||||
Format: string(s.Format),
|
||||
Path: s.Path,
|
||||
FileName: s.FileName,
|
||||
MaxBytes: s.RotateBytes,
|
||||
MaxDuration: s.RotateDuration,
|
||||
MaxFiles: s.RotateMaxFiles,
|
||||
}
|
||||
id, err = newId(fmt.Sprintf("file_%s_%s_", s.Path, s.FileName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
sinkId = eventlogger.NodeID(id)
|
||||
}
|
||||
err = e.broker.RegisterNode(sinkId, sinkNode)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("failed to register sink node %s", sinkId)))
|
||||
}
|
||||
var addToAudit, addToObservation, addToErr bool
|
||||
for _, t := range s.EventTypes {
|
||||
switch t {
|
||||
case EveryType:
|
||||
addToAudit = true
|
||||
addToObservation = true
|
||||
addToErr = true
|
||||
case ErrorType:
|
||||
addToErr = true
|
||||
case AuditType:
|
||||
addToAudit = true
|
||||
case ObservationType:
|
||||
addToObservation = true
|
||||
}
|
||||
}
|
||||
if addToAudit {
|
||||
auditPipelines = append(auditPipelines, pipeline{
|
||||
eventType: AuditType,
|
||||
fmtId: jsonfmtId,
|
||||
sinkId: sinkId,
|
||||
})
|
||||
}
|
||||
if addToObservation {
|
||||
observationPipelines = append(observationPipelines, pipeline{
|
||||
eventType: ObservationType,
|
||||
fmtId: jsonfmtId,
|
||||
sinkId: sinkId,
|
||||
})
|
||||
}
|
||||
if addToErr {
|
||||
errPipelines = append(errPipelines, pipeline{
|
||||
eventType: ErrorType,
|
||||
fmtId: jsonfmtId,
|
||||
sinkId: sinkId,
|
||||
})
|
||||
}
|
||||
}
|
||||
if c.AuditEnabled && len(auditPipelines) == 0 {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "audit events enabled but no sink defined for it")
|
||||
}
|
||||
if c.ObservationsEnabled && len(observationPipelines) == 0 {
|
||||
return nil, errors.New(errors.InvalidParameter, op, "observation events enabled but no sink defined for it")
|
||||
}
|
||||
|
||||
auditNodeIds := make([]eventlogger.NodeID, 0, len(auditPipelines))
|
||||
for _, p := range auditPipelines {
|
||||
gatedFilterNode := eventlogger.GatedFilter{}
|
||||
e.flushableNodes = append(e.flushableNodes, &gatedFilterNode)
|
||||
gateId, err := newId("gated-audit")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
gatedFilterNodeId := eventlogger.NodeID(gateId)
|
||||
if err := e.broker.RegisterNode(gatedFilterNodeId, &gatedFilterNode); err != nil {
|
||||
return nil, errors.Wrap(err, op, errors.WithMsg("unable to register audit gated filter"))
|
||||
}
|
||||
|
||||
pipeId, err := newId(auditPipeline)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
err = e.broker.RegisterPipeline(eventlogger.Pipeline{
|
||||
EventType: eventlogger.EventType(p.eventType),
|
||||
PipelineID: eventlogger.PipelineID(pipeId),
|
||||
NodeIDs: []eventlogger.NodeID{gatedFilterNodeId, p.fmtId, p.sinkId},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to register audit pipeline")
|
||||
}
|
||||
auditNodeIds = append(auditNodeIds, p.sinkId)
|
||||
}
|
||||
observationNodeIds := make([]eventlogger.NodeID, 0, len(observationPipelines))
|
||||
for _, p := range observationPipelines {
|
||||
gatedFilterNode := eventlogger.GatedFilter{}
|
||||
e.flushableNodes = append(e.flushableNodes, &gatedFilterNode)
|
||||
gateId, err := newId("gated-observation")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
gatedFilterNodeId := eventlogger.NodeID(gateId)
|
||||
if err := e.broker.RegisterNode(gatedFilterNodeId, &gatedFilterNode); err != nil {
|
||||
return nil, errors.Wrap(err, op, errors.WithMsg("unable to register audit gated filter"))
|
||||
}
|
||||
|
||||
pipeId, err := newId(observationPipeline)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
err = e.broker.RegisterPipeline(eventlogger.Pipeline{
|
||||
EventType: eventlogger.EventType(p.eventType),
|
||||
PipelineID: eventlogger.PipelineID(pipeId),
|
||||
NodeIDs: []eventlogger.NodeID{gatedFilterNodeId, p.fmtId, p.sinkId},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to register observation pipeline")
|
||||
}
|
||||
observationNodeIds = append(observationNodeIds, p.sinkId)
|
||||
}
|
||||
errNodeIds := make([]eventlogger.NodeID, 0, len(errPipelines))
|
||||
for _, p := range errPipelines {
|
||||
pipeId, err := newId(errPipeline)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, op)
|
||||
}
|
||||
err = e.broker.RegisterPipeline(eventlogger.Pipeline{
|
||||
EventType: eventlogger.EventType(p.eventType),
|
||||
PipelineID: eventlogger.PipelineID(pipeId),
|
||||
NodeIDs: []eventlogger.NodeID{p.fmtId, p.sinkId},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to register err pipeline")
|
||||
}
|
||||
errNodeIds = append(errNodeIds, p.sinkId)
|
||||
}
|
||||
|
||||
// TODO(jimlambrt) go-eventlogger SetSuccessThreshold currently does not
|
||||
// specify which sink passed and which hasn't so we are unable to
|
||||
// support multiple sinks with different delivery guarantees
|
||||
if c.AuditDelivery == Enforced {
|
||||
err = e.broker.SetSuccessThreshold(eventlogger.EventType(AuditType), len(auditNodeIds))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set success threshold for audit events")
|
||||
}
|
||||
}
|
||||
if c.ObservationDelivery == Enforced {
|
||||
err = e.broker.SetSuccessThreshold(eventlogger.EventType(ObservationType), len(observationNodeIds))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set success threshold for observation events")
|
||||
}
|
||||
}
|
||||
// always enforce delivery of errors
|
||||
err = e.broker.SetSuccessThreshold(eventlogger.EventType(ErrorType), len(errNodeIds))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set success threshold for error events")
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// writeObservation writes/sends an Observation event.
|
||||
func (e *Eventer) writeObservation(ctx context.Context, event *observation) error {
|
||||
const op = "event.(Eventer).writeObservation"
|
||||
if event == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing event")
|
||||
}
|
||||
if !e.conf.ObservationsEnabled {
|
||||
return nil
|
||||
}
|
||||
err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) {
|
||||
if event.Header != nil {
|
||||
event.Header[RequestInfoField] = event.RequestInfo
|
||||
event.Header[VersionField] = event.Version
|
||||
}
|
||||
if event.Detail != nil {
|
||||
event.Detail[OpField] = string(event.Op)
|
||||
}
|
||||
return e.broker.Send(ctx, eventlogger.EventType(ObservationType), event.SimpleGatedPayload)
|
||||
})
|
||||
if err != nil {
|
||||
e.logger.Error("encountered an error sending an observation event", "error:", err.Error())
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeError writes/sends an Err event
|
||||
func (e *Eventer) writeError(ctx context.Context, event *err) error {
|
||||
const op = "event.(Eventer).writeError"
|
||||
if event == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing event")
|
||||
}
|
||||
err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) {
|
||||
return e.broker.Send(ctx, eventlogger.EventType(ErrorType), event)
|
||||
})
|
||||
if err != nil {
|
||||
e.logger.Error("encountered an error sending an error event", "error:", err.Error())
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeAudit writes/send an audit event
|
||||
func (e *Eventer) writeAudit(ctx context.Context, event *audit) error {
|
||||
const op = "event.(Eventer).writeAudit"
|
||||
if event == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing event")
|
||||
}
|
||||
if !e.conf.AuditEnabled {
|
||||
return nil
|
||||
}
|
||||
err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) {
|
||||
return e.broker.Send(ctx, eventlogger.EventType(AuditType), event)
|
||||
})
|
||||
if err != nil {
|
||||
e.logger.Error("encountered an error sending an audit event", "error:", err.Error())
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reopen can used during a SIGHUP to reopen nodes, most importantly the underlying
|
||||
// file sinks.
|
||||
func (e *Eventer) Reopen() error {
|
||||
if e.broker != nil {
|
||||
return e.broker.Reopen(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlushNodes will flush any of the eventer's flushable nodes. This
|
||||
// needs to be called whenever Boundary is stopping (aka shutting down).
|
||||
func (e *Eventer) FlushNodes(ctx context.Context) error {
|
||||
const op = "event.(Eventer).FlushNodes"
|
||||
for _, n := range e.flushableNodes {
|
||||
if err := n.FlushAll(ctx); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
// EventerConfig supplies all the configuration needed to create/config an Eventer.
|
||||
type EventerConfig struct {
|
||||
AuditDelivery DeliveryGuarantee // AuditDelivery specifies the delivery guarantees for audit events (enforced or best effort).
|
||||
ObservationDelivery DeliveryGuarantee // ObservationDelivery specifies the delivery guarantees for observation events (enforced or best effort).
|
||||
AuditEnabled bool // AuditEnabled specifies if audit events should be emitted.
|
||||
ObservationsEnabled bool // ObservationsEnabled specifies if observation events should be emitted.
|
||||
Sinks []SinkConfig // Sinks are all the configured sinks
|
||||
}
|
||||
|
||||
// validate will validate the config. BTW, a config isn't required to have any
|
||||
// sinks to be valid.
|
||||
func (c *EventerConfig) validate() error {
|
||||
const op = "event.(EventerConfig).validate"
|
||||
|
||||
if err := c.AuditDelivery.validate(); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
if err := c.ObservationDelivery.validate(); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
for i, s := range c.Sinks {
|
||||
if err := s.validate(); err != nil {
|
||||
return errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("sink %d is invalid", i)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEventerConfig_validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c EventerConfig
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "invalid-audit-delivery",
|
||||
c: EventerConfig{
|
||||
AuditDelivery: "invalid",
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "is not a valid delivery guarantee",
|
||||
},
|
||||
{
|
||||
name: "valid-audit-delivery",
|
||||
c: EventerConfig{
|
||||
AuditDelivery: Enforced,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid-observation-delivery",
|
||||
c: EventerConfig{
|
||||
ObservationDelivery: "invalid",
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "is not a valid delivery guarantee",
|
||||
},
|
||||
{
|
||||
name: "valid-observation-delivery",
|
||||
c: EventerConfig{
|
||||
ObservationDelivery: Enforced,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid-sink",
|
||||
c: EventerConfig{
|
||||
Sinks: []SinkConfig{
|
||||
{
|
||||
SinkType: "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "is not a valid sink type",
|
||||
},
|
||||
{
|
||||
name: "valid-with-all-defaults",
|
||||
c: EventerConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
err := tt.c.validate()
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
const (
|
||||
// stdRetryCount is the standard number of times for retry when sending events
|
||||
stdRetryCount = 3
|
||||
)
|
||||
|
||||
type backoff interface {
|
||||
duration(attemptNumber uint) time.Duration
|
||||
}
|
||||
|
||||
type expBackoff struct{}
|
||||
|
||||
// duration returns an exponential backing off time duration
|
||||
func (b expBackoff) duration(attempt uint) time.Duration {
|
||||
r := rand.Float64()
|
||||
return time.Millisecond * time.Duration(math.Exp2(float64(attempt))*5*(r+0.5))
|
||||
}
|
||||
|
||||
type retryInfo struct {
|
||||
retries int
|
||||
backoff time.Duration
|
||||
}
|
||||
|
||||
type sendHandler func() (eventlogger.Status, error)
|
||||
|
||||
// retrySend will attempt sendHandler (which is intended to be a closure that
|
||||
// sends an event) the specified number of retries using the specified backoff.
|
||||
func (e *Eventer) retrySend(ctx context.Context, retries uint, backOff backoff, handler sendHandler) error {
|
||||
const op = "event.(Eventer).retrySend"
|
||||
if backOff == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing backoff")
|
||||
}
|
||||
if handler == nil {
|
||||
return errors.New(errors.InvalidParameter, op, "missing handler")
|
||||
}
|
||||
success := false
|
||||
var retryErrors error
|
||||
var attemptStatus eventlogger.Status
|
||||
info := retryInfo{}
|
||||
for attempts := uint(1); ; attempts++ {
|
||||
if attempts > retries+1 {
|
||||
retryErrors = multierror.Append(retryErrors, errors.New(errors.MaxRetries, op, fmt.Sprintf("Too many retries: reached max of %d", retries)))
|
||||
return retryErrors
|
||||
}
|
||||
var err error
|
||||
attemptStatus, err = handler()
|
||||
if len(attemptStatus.Warnings) > 0 {
|
||||
var retryWarnings error
|
||||
for _, w := range attemptStatus.Warnings {
|
||||
retryWarnings = multierror.Append(retryWarnings, w)
|
||||
}
|
||||
e.logger.Error("unable to send event", "operation", op, "warning", retryWarnings)
|
||||
}
|
||||
if err != nil {
|
||||
retryErrors = multierror.Append(retryErrors, errors.Wrap(err, op))
|
||||
d := backOff.duration(attempts)
|
||||
info.retries++
|
||||
info.backoff = info.backoff + d
|
||||
time.Sleep(d)
|
||||
continue
|
||||
}
|
||||
success = true
|
||||
break
|
||||
}
|
||||
if !success {
|
||||
return errors.Wrap(retryErrors, op, errors.WithMsg("failed to send event"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEventer_retrySend(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
testConfig := TestEventerConfig(t, "TestEventer_retrySend")
|
||||
|
||||
eventer, err := NewEventer(hclog.Default(), testConfig.EventerConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
testError := errors.New(errors.InvalidParameter, "missing-operation", "missing operation")
|
||||
testEvent, err := newError("TestEventer_retrySend", testError, WithId("test-error"))
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
retries uint
|
||||
backOff backoff
|
||||
handler sendHandler
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContain string
|
||||
}{
|
||||
{
|
||||
name: "missing-backoff",
|
||||
retries: 1,
|
||||
handler: func() (eventlogger.Status, error) {
|
||||
return eventer.broker.Send(ctx, eventlogger.EventType(ErrorType), testEvent)
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContain: "missing backoff",
|
||||
},
|
||||
{
|
||||
name: "missing-handler",
|
||||
retries: 1,
|
||||
backOff: expBackoff{},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContain: "missing handler",
|
||||
},
|
||||
{
|
||||
name: "too-many-retries",
|
||||
retries: 3,
|
||||
backOff: expBackoff{},
|
||||
handler: func() (eventlogger.Status, error) {
|
||||
return eventlogger.Status{}, errors.New(errors.InvalidParameter, "TestEventer_retrySend", "will never work")
|
||||
},
|
||||
wantErrMatch: errors.T(errors.MaxRetries),
|
||||
wantErrContain: "Too many retries",
|
||||
},
|
||||
{
|
||||
name: "success-with-warnings",
|
||||
retries: 3,
|
||||
backOff: expBackoff{},
|
||||
handler: func() (eventlogger.Status, error) {
|
||||
return eventlogger.Status{
|
||||
Warnings: []error{errors.New(errors.RecordNotFound, "TestEventer_retrySend", "not found")},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
retries: 1,
|
||||
backOff: expBackoff{},
|
||||
handler: func() (eventlogger.Status, error) {
|
||||
return eventlogger.Status{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
defer os.Remove(testConfig.AllEvents.Name())
|
||||
defer os.Remove(testConfig.ErrorEvents.Name())
|
||||
|
||||
err := eventer.retrySend(ctx, tt.retries, tt.backOff, tt.handler)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
multi, isMultiError := err.(*multierror.Error)
|
||||
switch isMultiError {
|
||||
case true:
|
||||
matched := false
|
||||
for _, e := range multi.WrappedErrors() {
|
||||
if errors.Match(tt.wantErrMatch, e) {
|
||||
if tt.wantErrContain != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContain)
|
||||
}
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
assert.True(matched)
|
||||
default:
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
if tt.wantErrContain != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContain)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,492 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_InitSysEventer(t *testing.T) {
|
||||
// this test and its subtests cannot be run in parallel because of it's
|
||||
// dependency on the sysEventer
|
||||
testConfig := TestEventerConfig(t, "InitSysEventer")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
log hclog.Logger
|
||||
config EventerConfig
|
||||
want *Eventer
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
|
||||
{
|
||||
name: "missing-hclog",
|
||||
config: testConfig.EventerConfig,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
config: testConfig.EventerConfig,
|
||||
log: hclog.Default(),
|
||||
want: &Eventer{
|
||||
logger: hclog.Default(),
|
||||
conf: testConfig.EventerConfig,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success-with-default-config",
|
||||
config: EventerConfig{},
|
||||
log: hclog.Default(),
|
||||
want: &Eventer{
|
||||
logger: hclog.Default(),
|
||||
conf: EventerConfig{
|
||||
Sinks: []SinkConfig{
|
||||
{
|
||||
Name: "default",
|
||||
EventTypes: []Type{EveryType},
|
||||
Format: JSONSinkFormat,
|
||||
SinkType: StdoutSink,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer TestResetSystEventer(t)
|
||||
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
err := InitSysEventer(tt.log, tt.config)
|
||||
got := SysEventer()
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Nil(got)
|
||||
require.Error(err)
|
||||
if tt.wantErrMatch != nil {
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
tt.want.broker = got.broker
|
||||
tt.want.flushableNodes = got.flushableNodes
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventer_writeObservation(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
testSetup := TestEventerConfig(t, "TestEventer_writeObservation")
|
||||
eventer, err := NewEventer(hclog.Default(), testSetup.EventerConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
testHeader := map[string]interface{}{"name": "header"}
|
||||
testDetail := map[string]interface{}{"name": "details"}
|
||||
testObservation, err := newObservation("Test_NewEventer", WithHeader(testHeader), WithDetails(testDetail))
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
broker broker
|
||||
observation *observation
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
{
|
||||
name: "missing-observation",
|
||||
broker: &testMockBroker{},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "send-fails",
|
||||
broker: &testMockBroker{errorOnSend: errors.New(errors.Io, "test", "no msg")},
|
||||
observation: testObservation,
|
||||
wantErrMatch: errors.T(errors.Io),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
broker: &testMockBroker{},
|
||||
observation: testObservation,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
eventer.broker = tt.broker
|
||||
|
||||
err = eventer.writeObservation(ctx, tt.observation)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "got %q and wanted %q", err.Error(), tt.wantErrMatch)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
t.Run("e2e", func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test",
|
||||
})
|
||||
c := EventerConfig{
|
||||
ObservationsEnabled: true,
|
||||
}
|
||||
// with no defined config, it will default to a stdout sink
|
||||
e, err := NewEventer(logger, c)
|
||||
require.NoError(err)
|
||||
|
||||
m := map[string]interface{}{
|
||||
"name": "bar",
|
||||
"list": []string{"1", "2"},
|
||||
}
|
||||
observationEvent, err := newObservation("Test_NewEventer", WithHeader(m))
|
||||
require.NoError(err)
|
||||
|
||||
require.NoError(e.writeObservation(context.Background(), observationEvent))
|
||||
})
|
||||
}
|
||||
|
||||
func TestEventer_writeAudit(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
testSetup := TestEventerConfig(t, "Test_NewEventer")
|
||||
eventer, err := NewEventer(hclog.Default(), testSetup.EventerConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
testAudit, err := newAudit(
|
||||
"TestEventer_writeAudit",
|
||||
WithRequestInfo(TestRequestInfo(t)),
|
||||
WithAuth(testAuth(t)),
|
||||
WithRequest(testRequest(t)),
|
||||
WithResponse(testResponse(t)))
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
broker broker
|
||||
audit *audit
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
{
|
||||
name: "missing-audit",
|
||||
broker: &testMockBroker{},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "send-fails",
|
||||
broker: &testMockBroker{errorOnSend: errors.New(errors.Io, "test", "no msg")},
|
||||
audit: testAudit,
|
||||
wantErrMatch: errors.T(errors.Io),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
broker: &testMockBroker{},
|
||||
audit: testAudit,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
eventer.broker = tt.broker
|
||||
|
||||
err = eventer.writeAudit(ctx, tt.audit)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "got %q and wanted %q", err.Error(), tt.wantErrMatch)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventer_writeError(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
testSetup := TestEventerConfig(t, "Test_NewEventer")
|
||||
eventer, er := NewEventer(hclog.Default(), testSetup.EventerConfig)
|
||||
require.NoError(t, er)
|
||||
|
||||
testError, er := newError("TestEventer_writeError", errors.New(errors.Io, "test", "no msg"))
|
||||
require.NoError(t, er)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
broker broker
|
||||
err *err
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
{
|
||||
name: "missing-error",
|
||||
broker: &testMockBroker{},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "send-fails",
|
||||
broker: &testMockBroker{errorOnSend: errors.New(errors.Io, "test", "no msg")},
|
||||
err: testError,
|
||||
wantErrMatch: errors.T(errors.Io),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
broker: &testMockBroker{},
|
||||
err: testError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
eventer.broker = tt.broker
|
||||
|
||||
err := eventer.writeError(ctx, tt.err)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "got %q and wanted %q", err.Error(), tt.wantErrMatch)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewEventer(t *testing.T) {
|
||||
t.Parallel()
|
||||
testSetup := TestEventerConfig(t, "Test_NewEventer")
|
||||
|
||||
testSetupWithOpts := TestEventerConfig(t, "Test_NewEventer", testWithAuditSink(), testWithObservationSink())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config EventerConfig
|
||||
opts []Option
|
||||
logger hclog.Logger
|
||||
want *Eventer
|
||||
wantRegistered []string
|
||||
wantPipelines []string
|
||||
wantThresholds map[eventlogger.EventType]int
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
{
|
||||
name: "invalid-config",
|
||||
config: EventerConfig{
|
||||
AuditDelivery: "invalid",
|
||||
},
|
||||
logger: hclog.Default(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "missing-logger",
|
||||
config: testSetup.EventerConfig,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "success-with-default-config",
|
||||
config: EventerConfig{},
|
||||
logger: hclog.Default(),
|
||||
want: &Eventer{
|
||||
logger: hclog.Default(),
|
||||
conf: EventerConfig{
|
||||
Sinks: []SinkConfig{
|
||||
{
|
||||
Name: "default",
|
||||
EventTypes: []Type{EveryType},
|
||||
Format: JSONSinkFormat,
|
||||
SinkType: StdoutSink,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantRegistered: []string{
|
||||
"json", // fmt for everything
|
||||
"stdout", // stdout
|
||||
"gated-observation", // stdout
|
||||
"gated-audit", // stdout
|
||||
},
|
||||
wantPipelines: []string{
|
||||
"audit", // stdout
|
||||
"observation", // stdout
|
||||
"error", // stdout
|
||||
},
|
||||
wantThresholds: map[eventlogger.EventType]int{
|
||||
"error": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "testSetup",
|
||||
config: testSetup.EventerConfig,
|
||||
logger: hclog.Default(),
|
||||
want: &Eventer{
|
||||
logger: hclog.Default(),
|
||||
conf: testSetup.EventerConfig,
|
||||
},
|
||||
wantRegistered: []string{
|
||||
"json", // fmt for everything
|
||||
"stdout", // stdout
|
||||
"gated-observation", // stdout
|
||||
"gated-audit", // stdout
|
||||
"tmp-all-events", // every-type-file-sync
|
||||
"gated-observation", // every-type-file-sync
|
||||
"gated-audit", // every-type-file-sync
|
||||
"tmp-errors", // error-file-sink
|
||||
},
|
||||
wantPipelines: []string{
|
||||
"audit", // every-type-file-sync
|
||||
"audit", // stdout
|
||||
"observation", // every-type-file-sync
|
||||
"observation", // stdout
|
||||
"error", // every-type-file-sync
|
||||
"error", // stdout
|
||||
"error", // error-file-sink
|
||||
},
|
||||
wantThresholds: map[eventlogger.EventType]int{
|
||||
"audit": 2,
|
||||
"error": 3,
|
||||
"observation": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "testSetup-with-all-opts",
|
||||
config: testSetupWithOpts.EventerConfig,
|
||||
logger: hclog.Default(),
|
||||
want: &Eventer{
|
||||
logger: hclog.Default(),
|
||||
conf: testSetupWithOpts.EventerConfig,
|
||||
},
|
||||
wantRegistered: []string{
|
||||
"json", // fmt for everything
|
||||
"stdout", // stdout
|
||||
"gated-observation", // stdout
|
||||
"gated-audit", // stdout
|
||||
"tmp-all-events", // every-type-file-sync
|
||||
"gated-observation", // every-type-file-sync
|
||||
"gated-audit", // every-type-file-sync
|
||||
"tmp-errors", // error-file-sink
|
||||
"gated-observation", // observation-file-sink
|
||||
"tmp-observation", // observations-file-sink
|
||||
"gated-audit", // audit-file-sink
|
||||
"tmp-audit", // audit-file-sink
|
||||
},
|
||||
wantPipelines: []string{
|
||||
"audit", // every-type-file-sync
|
||||
"audit", // stdout
|
||||
"observation", // every-type-file-sync
|
||||
"observation", // stdout
|
||||
"error", // every-type-file-sync
|
||||
"error", // stdout
|
||||
"error", // error-file-sink
|
||||
"audit", // audit-file-sink
|
||||
"observation", // observation-file-sink
|
||||
},
|
||||
wantThresholds: map[eventlogger.EventType]int{
|
||||
"audit": 3,
|
||||
"error": 3,
|
||||
"observation": 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
testBroker := &testMockBroker{}
|
||||
got, err := NewEventer(tt.logger, tt.config, testWithBroker(testBroker))
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
require.Nil(got)
|
||||
assert.True(errors.Match(tt.wantErrMatch, err))
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
tt.want.broker = got.broker
|
||||
tt.want.flushableNodes = got.flushableNodes
|
||||
assert.Equal(tt.want, got)
|
||||
|
||||
assert.Lenf(testBroker.registeredNodeIds, len(tt.wantRegistered), "got nodes: %q", testBroker.registeredNodeIds)
|
||||
for _, want := range tt.wantRegistered {
|
||||
found := false
|
||||
for _, got := range testBroker.registeredNodeIds {
|
||||
if strings.Contains(string(got), want) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(found, "did not find %s in the registered nodes: %s", want, testBroker.registeredNodeIds)
|
||||
}
|
||||
assert.Lenf(testBroker.pipelines, len(tt.wantPipelines), "got pipelines: %q", testBroker.pipelines)
|
||||
for _, want := range tt.wantPipelines {
|
||||
found := false
|
||||
for _, got := range testBroker.pipelines {
|
||||
if strings.Contains(string(got.EventType), want) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Truef(found, "did not find %s in the registered pipelines: %s", want, testBroker.pipelines)
|
||||
}
|
||||
|
||||
assert.Equal(tt.wantThresholds, testBroker.successThresholds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventer_Reopen(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
e, err := NewEventer(hclog.Default(), EventerConfig{})
|
||||
require.NoError(err)
|
||||
|
||||
e.broker = nil
|
||||
require.NoError(e.Reopen())
|
||||
|
||||
e.broker = &testMockBroker{}
|
||||
require.NoError(e.Reopen())
|
||||
assert.True(e.broker.(*testMockBroker).reopened)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEventer_FlushNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
e, err := NewEventer(hclog.Default(), EventerConfig{})
|
||||
require.NoError(err)
|
||||
|
||||
node := &testFlushNode{}
|
||||
e.flushableNodes = append(e.flushableNodes, node)
|
||||
require.NoError(e.FlushNodes(context.Background()))
|
||||
|
||||
node.raiseError = true
|
||||
require.Error(e.FlushNodes(context.Background()))
|
||||
assert.True(node.flushed)
|
||||
})
|
||||
}
|
||||
|
||||
type testFlushNode struct {
|
||||
flushed bool
|
||||
raiseError bool
|
||||
}
|
||||
|
||||
func (t *testFlushNode) FlushAll(_ context.Context) error {
|
||||
t.flushed = true
|
||||
if t.raiseError {
|
||||
return errors.New(errors.InvalidParameter, "flush-all", "test error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
func newId(prefix string) (string, error) {
|
||||
const op = "event.newId"
|
||||
id, err := db.NewPublicId(prefix)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, op)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newId(t *testing.T) {
|
||||
t.Run("basics", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := newId("pre")
|
||||
require.NoError(err)
|
||||
assert.True(strings.HasPrefix(got, "pre"))
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct.
|
||||
func getOpts(opt ...Option) options {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
o(&opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments.
|
||||
type Option func(*options)
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withId string
|
||||
withDetails map[string]interface{}
|
||||
withHeader map[string]interface{}
|
||||
withFlush bool
|
||||
withRequestInfo *RequestInfo
|
||||
withNow time.Time
|
||||
withRequest *Request
|
||||
withResponse *Response
|
||||
withAuth *Auth
|
||||
|
||||
withBroker broker // test only option
|
||||
withAuditSink bool // test only option
|
||||
withObservationSink bool // test only option
|
||||
}
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{}
|
||||
}
|
||||
|
||||
// WithId allows an optional Id
|
||||
func WithId(id string) Option {
|
||||
return func(o *options) {
|
||||
o.withId = id
|
||||
}
|
||||
}
|
||||
|
||||
// WithDetails allows an optional map as details
|
||||
func WithDetails(d map[string]interface{}) Option {
|
||||
return func(o *options) {
|
||||
o.withDetails = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithHeader allows an optional map as a header
|
||||
func WithHeader(d map[string]interface{}) Option {
|
||||
return func(o *options) {
|
||||
o.withHeader = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithFlush allows an optional flush option.
|
||||
func WithFlush() Option {
|
||||
return func(o *options) {
|
||||
o.withFlush = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequestInfo allows an optional RequestInfo
|
||||
func WithRequestInfo(i *RequestInfo) Option {
|
||||
return func(o *options) {
|
||||
o.withRequestInfo = i
|
||||
}
|
||||
}
|
||||
|
||||
// WithNow allows an option time.Time to represent now.
|
||||
func WithNow(now time.Time) Option {
|
||||
return func(o *options) {
|
||||
o.withNow = now
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequest allows an optional request
|
||||
func WithRequest(r *Request) Option {
|
||||
return func(o *options) {
|
||||
o.withRequest = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithResponse allows an optional response
|
||||
func WithResponse(r *Response) Option {
|
||||
return func(o *options) {
|
||||
o.withResponse = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuth allows an optional Auth
|
||||
func WithAuth(a *Auth) Option {
|
||||
return func(o *options) {
|
||||
o.withAuth = a
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test_GetOpts provides unit tests for GetOpts and all the options
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("WithId", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(WithId("test"))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withId = "test"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithDetails", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
d := map[string]interface{}{
|
||||
"name": "alice",
|
||||
}
|
||||
opts := getOpts(WithDetails(d))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withDetails = d
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithHeader", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
h := map[string]interface{}{
|
||||
"name": "alice",
|
||||
}
|
||||
opts := getOpts(WithHeader(h))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withHeader = h
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithFlush", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts := getOpts(WithFlush())
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withFlush = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithRequestInfo", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
info := TestRequestInfo(t)
|
||||
opts := getOpts(WithRequestInfo(info))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withRequestInfo = info
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithNow", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
now := time.Now()
|
||||
opts := getOpts(WithNow(now))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withNow = now
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithRequest", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
r := testRequest(t)
|
||||
opts := getOpts(WithRequest(r))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withRequest = r
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithResponse", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
r := testResponse(t)
|
||||
opts := getOpts(WithResponse(r))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withResponse = r
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithAuth", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
auth := testAuth(t)
|
||||
opts := getOpts(WithAuth(auth))
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withAuth = auth
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
// SinkConfig defines the configuration for a Eventer sink
|
||||
type SinkConfig struct {
|
||||
Name string // Name defines a name for the sink.
|
||||
EventTypes []Type // 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.
|
||||
SinkType SinkType // SinkType defines the type of sink (StdoutSink or FileSink)
|
||||
Format SinkFormat // Format defines the format for the sink (JSONSinkFormat)
|
||||
Path string // Path defines the file path for the sink
|
||||
FileName string // FileName defines the file name for the sink
|
||||
RotateBytes int // RotateByes defines the number of bytes that should trigger rotation of a FileSink
|
||||
RotateDuration time.Duration // RotateDuration defines how often a FileSink should be rotated
|
||||
RotateMaxFiles int // RotateMaxFiles defines how may historical rotated files should be kept for a FileSink
|
||||
}
|
||||
|
||||
func (sc *SinkConfig) validate() error {
|
||||
const op = "event.(SinkConfig).validate"
|
||||
if err := sc.SinkType.validate(); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
if err := sc.Format.validate(); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
if sc.SinkType == FileSink && sc.FileName == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing sink file name")
|
||||
}
|
||||
if sc.Name == "" {
|
||||
return errors.New(errors.InvalidParameter, op, "missing sink name")
|
||||
}
|
||||
if len(sc.EventTypes) == 0 {
|
||||
return errors.New(errors.InvalidParameter, op, "missing event types")
|
||||
}
|
||||
for _, et := range sc.EventTypes {
|
||||
if err := et.validate(); err != nil {
|
||||
return errors.Wrap(err, op)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSinkConfig_validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
sc SinkConfig
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-name",
|
||||
sc: SinkConfig{
|
||||
EventTypes: []Type{EveryType},
|
||||
SinkType: FileSink,
|
||||
FileName: "tmp.file",
|
||||
Format: JSONSinkFormat,
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing sink name",
|
||||
},
|
||||
{
|
||||
name: "missing-EventType",
|
||||
sc: SinkConfig{
|
||||
Name: "sink-name",
|
||||
SinkType: FileSink,
|
||||
FileName: "tmp.file",
|
||||
Format: JSONSinkFormat,
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing event types",
|
||||
},
|
||||
{
|
||||
name: "invalid-EventType",
|
||||
sc: SinkConfig{
|
||||
Name: "sink-name",
|
||||
EventTypes: []Type{"invalid"},
|
||||
SinkType: FileSink,
|
||||
FileName: "tmp.file",
|
||||
Format: JSONSinkFormat,
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not a valid event type",
|
||||
},
|
||||
{
|
||||
name: "missing-sink-type",
|
||||
sc: SinkConfig{
|
||||
Name: "sink-name",
|
||||
EventTypes: []Type{EveryType},
|
||||
FileName: "tmp.file",
|
||||
Format: JSONSinkFormat,
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not a valid sink type",
|
||||
},
|
||||
{
|
||||
name: "invalid-sink-type",
|
||||
sc: SinkConfig{
|
||||
Name: "sink-name",
|
||||
EventTypes: []Type{EveryType},
|
||||
SinkType: "invalid",
|
||||
FileName: "tmp.file",
|
||||
Format: JSONSinkFormat,
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not a valid sink type",
|
||||
},
|
||||
{
|
||||
name: "missing-format",
|
||||
sc: SinkConfig{
|
||||
Name: "sink-name",
|
||||
SinkType: FileSink,
|
||||
EventTypes: []Type{EveryType},
|
||||
FileName: "tmp.file",
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not a valid sink format",
|
||||
},
|
||||
{
|
||||
name: "invalid-format",
|
||||
sc: SinkConfig{
|
||||
Name: "sink-name",
|
||||
Format: "invalid",
|
||||
SinkType: FileSink,
|
||||
EventTypes: []Type{EveryType},
|
||||
FileName: "tmp.file",
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not a valid sink format",
|
||||
},
|
||||
{
|
||||
name: "file-sink-with-no-file-name",
|
||||
sc: SinkConfig{
|
||||
EventTypes: []Type{EveryType},
|
||||
SinkType: FileSink,
|
||||
Format: JSONSinkFormat,
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing sink file name",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
sc: SinkConfig{
|
||||
Name: "valid",
|
||||
EventTypes: []Type{EveryType},
|
||||
SinkType: FileSink,
|
||||
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()
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
require.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
JSONSinkFormat SinkFormat = "json" // JSONSinkFormat means the event is formatted as JSON
|
||||
)
|
||||
|
||||
type SinkFormat string // SinkFormat defines the formatting for a sink in a config file stanza (json)
|
||||
|
||||
func (f SinkFormat) validate() error {
|
||||
const op = "event.(SinkFormat).validate"
|
||||
switch f {
|
||||
case JSONSinkFormat:
|
||||
return nil
|
||||
default:
|
||||
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("'%s' is not a valid sink format", f))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
StdoutSink SinkType = "stdout" // StdoutSink is written to stdout
|
||||
FileSink SinkType = "file" // FileSink is written to a file
|
||||
)
|
||||
|
||||
type SinkType string // SinkType defines the type of sink in a config stanza (file, stdout)
|
||||
|
||||
func (t SinkType) validate() error {
|
||||
const op = "event.(SinkType).validate"
|
||||
switch t {
|
||||
case StdoutSink, FileSink:
|
||||
return nil
|
||||
default:
|
||||
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("'%s' is not a valid sink type", t))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,230 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/gen/controller/api/resources/groups"
|
||||
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
var testSysEventerLock sync.Mutex
|
||||
|
||||
// TestResetSysEventer will reset event.syseventer to an uninitialized state.
|
||||
func TestResetSystEventer(t *testing.T) {
|
||||
t.Helper()
|
||||
testSysEventerLock.Lock()
|
||||
defer testSysEventerLock.Unlock()
|
||||
sysEventerOnce = sync.Once{}
|
||||
sysEventer = nil
|
||||
}
|
||||
|
||||
type TestConfig struct {
|
||||
EventerConfig EventerConfig
|
||||
AllEvents *os.File
|
||||
ErrorEvents *os.File
|
||||
ObservationEvents *os.File
|
||||
AuditEvents *os.File
|
||||
}
|
||||
|
||||
// TestEventerConfig creates a test config and registers a cleanup func for its
|
||||
// test tmp files.
|
||||
func TestEventerConfig(t *testing.T, testName string, opt ...Option) TestConfig {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
tmpAllFile, err := ioutil.TempFile("./", "tmp-all-events-"+testName)
|
||||
require.NoError(err)
|
||||
|
||||
tmpErrFile, err := ioutil.TempFile("./", "tmp-errors-"+testName)
|
||||
require.NoError(err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.Remove(tmpAllFile.Name())
|
||||
os.Remove(tmpErrFile.Name())
|
||||
})
|
||||
|
||||
c := TestConfig{
|
||||
EventerConfig: EventerConfig{
|
||||
ObservationsEnabled: true,
|
||||
ObservationDelivery: Enforced,
|
||||
AuditEnabled: true,
|
||||
AuditDelivery: Enforced,
|
||||
Sinks: []SinkConfig{
|
||||
{
|
||||
Name: "every-type-file-sink",
|
||||
SinkType: FileSink,
|
||||
EventTypes: []Type{EveryType},
|
||||
Format: JSONSinkFormat,
|
||||
Path: "./",
|
||||
FileName: tmpAllFile.Name(),
|
||||
},
|
||||
{
|
||||
Name: "stdout",
|
||||
SinkType: StdoutSink,
|
||||
EventTypes: []Type{EveryType},
|
||||
Format: JSONSinkFormat,
|
||||
},
|
||||
{
|
||||
Name: "err-file-sink",
|
||||
SinkType: FileSink,
|
||||
EventTypes: []Type{ErrorType},
|
||||
Format: JSONSinkFormat,
|
||||
Path: "./",
|
||||
FileName: tmpErrFile.Name(),
|
||||
},
|
||||
},
|
||||
},
|
||||
AllEvents: tmpAllFile,
|
||||
ErrorEvents: tmpErrFile,
|
||||
}
|
||||
opts := getOpts(opt...)
|
||||
if opts.withAuditSink {
|
||||
tmpFile, err := ioutil.TempFile("./", "tmp-audit-"+testName)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
os.Remove(tmpFile.Name())
|
||||
})
|
||||
c.EventerConfig.Sinks = append(c.EventerConfig.Sinks, SinkConfig{
|
||||
Name: "audit-file-sink",
|
||||
SinkType: FileSink,
|
||||
EventTypes: []Type{AuditType},
|
||||
Format: JSONSinkFormat,
|
||||
Path: "./",
|
||||
FileName: tmpFile.Name(),
|
||||
})
|
||||
}
|
||||
if opts.withObservationSink {
|
||||
tmpFile, err := ioutil.TempFile("./", "tmp-observation-"+testName)
|
||||
require.NoError(err)
|
||||
t.Cleanup(func() {
|
||||
os.Remove(tmpFile.Name())
|
||||
})
|
||||
c.EventerConfig.Sinks = append(c.EventerConfig.Sinks, SinkConfig{
|
||||
Name: "err-observation-sink",
|
||||
SinkType: FileSink,
|
||||
EventTypes: []Type{ObservationType},
|
||||
Format: JSONSinkFormat,
|
||||
Path: "./",
|
||||
FileName: tmpFile.Name(),
|
||||
})
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// TestRequestInfo provides a test RequestInfo
|
||||
func TestRequestInfo(t *testing.T) *RequestInfo {
|
||||
t.Helper()
|
||||
return &RequestInfo{
|
||||
Id: "test-request-info",
|
||||
Method: "POST",
|
||||
Path: "/test/request/info",
|
||||
PublicId: "public-id",
|
||||
}
|
||||
}
|
||||
|
||||
func testAuth(t *testing.T) *Auth {
|
||||
t.Helper()
|
||||
return &Auth{
|
||||
UserEmail: "test-auth@example.com",
|
||||
UserName: "test-auth-user-name",
|
||||
}
|
||||
}
|
||||
|
||||
func testRequest(t *testing.T) *Request {
|
||||
t.Helper()
|
||||
return &Request{
|
||||
Operation: "op",
|
||||
Endpoint: "/group/<id>",
|
||||
Details: &pbs.GetGroupRequest{
|
||||
Id: "group-id",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testResponse(t *testing.T) *Response {
|
||||
t.Helper()
|
||||
return &Response{
|
||||
StatusCode: 200,
|
||||
Details: &pbs.GetGroupResponse{
|
||||
Item: &groups.Group{
|
||||
Id: "group-id",
|
||||
ScopeId: "org-id",
|
||||
Name: &wrapperspb.StringValue{
|
||||
Value: "group-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// testWithBroker is an unexported and a test option for passing in an optional broker
|
||||
func testWithBroker(b broker) Option {
|
||||
return func(o *options) {
|
||||
o.withBroker = b
|
||||
}
|
||||
}
|
||||
|
||||
// testWithObservationSink is an unexported and a test option
|
||||
func testWithObservationSink() Option {
|
||||
return func(o *options) {
|
||||
o.withObservationSink = true
|
||||
}
|
||||
}
|
||||
|
||||
// testWithAuditSink is an unexported and a test option
|
||||
func testWithAuditSink() Option {
|
||||
return func(o *options) {
|
||||
o.withAuditSink = true
|
||||
}
|
||||
}
|
||||
|
||||
type testMockBroker struct {
|
||||
reopened bool
|
||||
stopTimeAt time.Time
|
||||
registeredNodeIds []eventlogger.NodeID
|
||||
successThresholds map[eventlogger.EventType]int
|
||||
pipelines []eventlogger.Pipeline
|
||||
|
||||
errorOnSend error
|
||||
}
|
||||
|
||||
func (b *testMockBroker) Reopen(ctx context.Context) error {
|
||||
b.reopened = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *testMockBroker) RegisterPipeline(def eventlogger.Pipeline) error {
|
||||
b.pipelines = append(b.pipelines, def)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *testMockBroker) Send(ctx context.Context, t eventlogger.EventType, payload interface{}) (eventlogger.Status, error) {
|
||||
if b.errorOnSend != nil {
|
||||
return eventlogger.Status{}, b.errorOnSend
|
||||
}
|
||||
return eventlogger.Status{}, nil
|
||||
}
|
||||
|
||||
func (b *testMockBroker) StopTimeAt(t time.Time) {
|
||||
b.stopTimeAt = t
|
||||
}
|
||||
|
||||
func (b *testMockBroker) RegisterNode(id eventlogger.NodeID, node eventlogger.Node) error {
|
||||
b.registeredNodeIds = append(b.registeredNodeIds, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *testMockBroker) SetSuccessThreshold(t eventlogger.EventType, successThreshold int) error {
|
||||
if b.successThresholds == nil {
|
||||
b.successThresholds = map[eventlogger.EventType]int{}
|
||||
}
|
||||
b.successThresholds[t] = successThreshold
|
||||
return nil
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
NEXT_PUBLIC_ALGOLIA_APP_ID=YY0FFNI7MF
|
||||
NEXT_PUBLIC_ALGOLIA_INDEX=product_BOUNDARY
|
||||
NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=5037da4824714676226913c65e961ca0
|
||||
NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=8308498decdf72e11590fc6356e5fdde
|
||||
|
||||
Loading…
Reference in new issue