diff --git a/crowdsec/caddyfile_test.go b/crowdsec/caddyfile_test.go index f0dd811f..482ffdd8 100644 --- a/crowdsec/caddyfile_test.go +++ b/crowdsec/caddyfile_test.go @@ -13,139 +13,116 @@ import ( func TestUnmarshalCaddyfile(t *testing.T) { tv := true fv := false - type args struct { - d *caddyfile.Dispenser - } tests := []struct { name string + input string + env map[string]string expected *CrowdSec - args args wantParseErr bool }{ { - name: "fail/missing tokens", - expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(``), - }, + name: "fail/missing tokens", + expected: &CrowdSec{}, + input: ``, wantParseErr: true, }, { - name: "fail/not-crowdsec", - expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`not-crowdsec`), - }, + name: "fail/not-crowdsec", + expected: &CrowdSec{}, + input: `not-crowdsec`, wantParseErr: true, }, { name: "fail/invalid-duration", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key some_random_key ticker_interval 30x - }`), - }, + }`, wantParseErr: true, }, { name: "fail/no-api-url", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { - api_url - api_key some_random_key - ticker_interval 30x - }`), - }, + input: ` + crowdsec { + api_url + api_key some_random_key + ticker_interval 30x + }`, wantParseErr: true, }, { name: "fail/invalid-api-url", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://\x00/ api_key some_random_key ticker_interval 30x - }`), - }, + }`, wantParseErr: true, }, { name: "fail/invalid-api-url-no-scheme", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url example.com api_key some_random_key ticker_interval 30x - }`), - }, + }`, wantParseErr: true, }, { name: "fail/missing-api-key", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key - }`), - }, + }`, wantParseErr: true, }, { name: "fail/missing-ticker-interval", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key test-key ticker_interval - }`), - }, + }`, wantParseErr: true, }, { name: "fail/invalid-streaming", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key test-key ticker_interval 30s disable_streaming absolutely - }`), - }, + }`, wantParseErr: true, }, { name: "fail/invalid-streaming", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key test-key ticker_interval 30s disable_streaming enable_hard_fails yo - }`), - }, + }`, wantParseErr: true, }, { name: "fail/unknown-token", expected: &CrowdSec{}, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key some_random_key unknown_token 42 - }`), - }, + }`, wantParseErr: true, }, { @@ -157,12 +134,10 @@ func TestUnmarshalCaddyfile(t *testing.T) { EnableStreaming: &tv, EnableHardFails: &fv, }, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key some_random_key - }`), - }, + }`, wantParseErr: false, }, { @@ -174,25 +149,49 @@ func TestUnmarshalCaddyfile(t *testing.T) { EnableStreaming: &fv, EnableHardFails: &tv, }, - args: args{ - d: caddyfile.NewTestDispenser(`crowdsec { + input: `crowdsec { api_url http://127.0.0.1:8080 api_key some_random_key ticker_interval 33s disable_streaming enable_hard_fails - }`), + }`, + wantParseErr: false, + }, + { + name: "ok/env-vars", + expected: &CrowdSec{ + APIUrl: "http://127.0.0.2:8080/", + APIKey: "env-test-key", + TickerInterval: "25s", + EnableStreaming: &tv, + EnableHardFails: &fv, + }, + env: map[string]string{ + "CROWDSEC_TEST_API_URL": "http://127.0.0.2:8080/", + "CROWDSEC_TEST_API_KEY": "env-test-key", + "CROWDSEC_TEST_TICKER_INTERVAL": "25s", }, + input: `crowdsec { + api_url {$CROWDSEC_TEST_API_URL} + api_key {$CROWDSEC_TEST_API_KEY} + ticker_interval {$CROWDSEC_TEST_TICKER_INTERVAL} + }`, wantParseErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - jsonApp, err := parseCrowdSec(tt.args.d, nil) + for k, v := range tt.env { + t.Setenv(k, v) + } + dispenser := caddyfile.NewTestDispenser(tt.input) + jsonApp, err := parseCrowdSec(dispenser, nil) if tt.wantParseErr { assert.Error(t, err) return } + assert.NoError(t, err) app, ok := jsonApp.(httpcaddyfile.App) require.True(t, ok) diff --git a/crowdsec/crowdsec_test.go b/crowdsec/crowdsec_test.go index 61f5be71..67b72395 100644 --- a/crowdsec/crowdsec_test.go +++ b/crowdsec/crowdsec_test.go @@ -35,6 +35,7 @@ func TestCrowdSec_Provision(t *testing.T) { tests := []struct { name string config string + env map[string]string assertion func(tt assert.TestingT, c *CrowdSec) wantErr bool }{ @@ -68,6 +69,25 @@ func TestCrowdSec_Provision(t *testing.T) { }, wantErr: false, }, + { + name: "json-env-vars", + config: `{ + "api_url": "{env.CROWDSEC_TEST_API_URL}", + "api_key": "{env.CROWDSEC_TEST_API_KEY}", + "ticker_interval": "{env.CROWDSEC_TEST_TICKER_INTERVAL}" + }`, + env: map[string]string{ + "CROWDSEC_TEST_API_URL": "http://127.0.0.2:8080/", + "CROWDSEC_TEST_API_KEY": "env-test-key", + "CROWDSEC_TEST_TICKER_INTERVAL": "25s", + }, + assertion: func(tt assert.TestingT, c *CrowdSec) { + assert.Equal(tt, "http://127.0.0.2:8080/", c.APIUrl) + assert.Equal(tt, "env-test-key", c.APIKey) + assert.Equal(tt, "25s", c.TickerInterval) + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -75,6 +95,10 @@ func TestCrowdSec_Provision(t *testing.T) { err := json.Unmarshal([]byte(tt.config), &c) require.NoError(t, err) + for k, v := range tt.env { + t.Setenv(k, v) + } + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) err = c.Provision(ctx) require.NoError(t, err) diff --git a/go.mod b/go.mod index 8671b630..9f209614 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/mholt/caddy-l4 v0.0.0-20231016112149-a362a1fbf652 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 + go.uber.org/goleak v1.2.1 go.uber.org/zap v1.26.0 ) @@ -127,7 +128,6 @@ require ( go.step.sm/cli-utils v0.8.0 // indirect go.step.sm/crypto v0.36.1 // indirect go.step.sm/linkedca v0.20.1 // indirect - go.uber.org/goleak v1.2.1 // indirect go.uber.org/mock v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.14.0 // indirect diff --git a/internal/bouncer/bouncer.go b/internal/bouncer/bouncer.go index 61ea8cdd..c866c9e4 100644 --- a/internal/bouncer/bouncer.go +++ b/internal/bouncer/bouncer.go @@ -217,13 +217,15 @@ func (b *Bouncer) Run() { func (b *Bouncer) Shutdown() error { b.startMu.Lock() defer b.startMu.Unlock() + if !b.started || b.stopped { + return nil + } b.logger.Info("stopping", b.zapField()) defer func() { b.stopped = true + b.logger.Info("finished", b.zapField()) + b.logger.Sync() // nolint }() - if !b.started || b.stopped { - return nil - } // the LiveBouncer has nothing to do on shutdown if !b.useStreamingBouncer { @@ -233,9 +235,6 @@ func (b *Bouncer) Shutdown() error { b.cancel() b.wg.Wait() - b.logger.Info("finished", b.zapField()) - b.logger.Sync() // nolint - // TODO: clean shutdown of the streaming bouncer channel reading //b.store = nil // TODO(hs): setting this to nil without reinstantiating it, leads to errors; do this properly. return nil diff --git a/internal/bouncer/logging.go b/internal/bouncer/logging.go index d2edbd9d..a28e9255 100644 --- a/internal/bouncer/logging.go +++ b/internal/bouncer/logging.go @@ -3,28 +3,46 @@ package bouncer import ( "errors" "io" + "unicode" + "unicode/utf8" "github.com/sirupsen/logrus" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) +// overrideLogrusLogger overrides the (default) settings of the standard +// logrus logger. The logrus logger is used by the `go-cs-bouncer` package, +// whereas Caddy uses zap. The output of the standard logger is discarded, +// and a hook is used to send messages to Caddy's zap logger instead. +// +// Note that this method changes global state, but only after a new Bouncer +// is provisioned, validated and has just been started. This should thus +// generally not be a problem. func (b *Bouncer) overrideLogrusLogger() { + // the CrowdSec go-cs-bouncer uses the standard logrus logger + std := logrus.StandardLogger() + // silence the default CrowdSec logrus logging - logrus.SetOutput(io.Discard) + std.SetOutput(io.Discard) - // catch log entries and log them using the *zap.Logger instead - logrus.AddHook(&zapAdapterHook{ + // replace all hooks on the standard logrus logger + hooks := logrus.LevelHooks{} + hooks.Add(&zapAdapterHook{ logger: b.logger, shouldFailHard: b.shouldFailHard, address: b.streamingBouncer.APIUrl, + instanceID: b.instanceID, }) + + std.ReplaceHooks(hooks) } type zapAdapterHook struct { logger *zap.Logger shouldFailHard bool address string + instanceID string } func (zh *zapAdapterHook) Levels() []logrus.Level { @@ -43,28 +61,40 @@ func (zh *zapAdapterHook) Fire(entry *logrus.Entry) error { // TODO: extract details from entry.Data? But doesn't seem to be used by CrowdSec today. msg := entry.Message - fields := []zapcore.Field{zap.String("address", zh.address)} + fields := []zapcore.Field{zap.String("instance_id", zh.instanceID), zap.String("address", zh.address)} switch { case entry.Level <= logrus.ErrorLevel: // error, fatal, panic fields = append(fields, zap.Error(errors.New(msg))) if zh.shouldFailHard { // TODO: if we keep this Fatal and the "shouldFailhard" around, ensure we // shut the bouncer down nicely - zh.logger.Fatal(msg, fields...) + zh.logger.Fatal(firstToLower(msg), fields...) } else { - zh.logger.Error(msg, fields...) + zh.logger.Error(firstToLower(msg), fields...) } default: level := zapcore.DebugLevel if l, ok := levelAdapter[entry.Level]; ok { level = l } - zh.logger.Log(level, msg, fields...) + zh.logger.Log(level, firstToLower(msg), fields...) } return nil } +func firstToLower(s string) string { + r, size := utf8.DecodeRuneInString(s) + if r == utf8.RuneError && size <= 1 { + return s + } + lc := unicode.ToLower(r) + if r == lc { + return s + } + return string(lc) + s[size:] +} + var levelAdapter = map[logrus.Level]zapcore.Level{ logrus.TraceLevel: zapcore.DebugLevel, // no trace level in zap logrus.DebugLevel: zapcore.DebugLevel,