Skip to content

Commit

Permalink
Merge pull request #36 from hslatman/herman/fix-config
Browse files Browse the repository at this point in the history
Improve configuration loading and fix issues related to configuration (re)load
  • Loading branch information
hslatman authored Jan 13, 2024
2 parents 87ada7f + 28ef1be commit 054d8c9
Show file tree
Hide file tree
Showing 12 changed files with 626 additions and 225 deletions.
46 changes: 31 additions & 15 deletions crowdsec/caddyfile.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
package crowdsec

import (
"fmt"
"net/url"
"strings"
"time"

"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
)

func parseCaddyfileGlobalOption(d *caddyfile.Dispenser, existingVal interface{}) (interface{}, error) {

// TODO: make this work similar to the handler? Or doesn't that work for this
// app level module, because of shared config etc.

cfg = &config{
TickerInterval: defaultTickerInterval,
EnableStreaming: defaultStreamingEnabled,
EnableHardFails: defaultHardFailsEnabled,
func parseCrowdSec(d *caddyfile.Dispenser, existingVal any) (any, error) {
tv := true
fv := false
cs := &CrowdSec{
TickerInterval: "60s",
EnableStreaming: &tv,
EnableHardFails: &fv,
}

if !d.Next() {
return nil, d.Err("expected tokens")
}

if d.Val() != "crowdsec" {
return nil, d.Err(fmt.Sprintf(`expected "crowdsec"; got %q`, d.Val()))
}

for d.NextBlock(0) {
switch d.Val() {
case "api_url":
Expand All @@ -32,12 +38,19 @@ func parseCaddyfileGlobalOption(d *caddyfile.Dispenser, existingVal interface{})
if err != nil {
return nil, d.Errf("invalid URL %s: %v", d.Val(), err)
}
cfg.APIUrl = u.String()
if u.Scheme == "" {
return nil, d.Errf("URL %q does not have a scheme (i.e https)", u.String())
}
s := u.String()
if !strings.HasSuffix(s, "/") {
s = s + "/"
}
cs.APIUrl = s
case "api_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
cfg.APIKey = d.Val()
cs.APIKey = d.Val()
case "ticker_interval":
if !d.NextArg() {
return nil, d.ArgErr()
Expand All @@ -46,21 +59,24 @@ func parseCaddyfileGlobalOption(d *caddyfile.Dispenser, existingVal interface{})
if err != nil {
return nil, d.Errf("invalid duration %s: %v", d.Val(), err)
}
cfg.TickerInterval = interval.String()
cs.TickerInterval = interval.String()
case "disable_streaming":
if d.NextArg() {
return nil, d.ArgErr()
}
cfg.EnableStreaming = false
cs.EnableStreaming = &fv
case "enable_hard_fails":
if d.NextArg() {
return nil, d.ArgErr()
}
cfg.EnableHardFails = true
cs.EnableHardFails = &tv
default:
return nil, d.Errf("invalid configuration token provided: %s", d.Val())
}
}

return nil, nil
return httpcaddyfile.App{
Name: "crowdsec",
Value: caddyconfig.JSON(cs, nil),
}, nil
}
250 changes: 164 additions & 86 deletions crowdsec/caddyfile_test.go
Original file line number Diff line number Diff line change
@@ -1,133 +1,211 @@
package crowdsec

import (
"encoding/json"
"testing"

"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUnmarshalCaddyfile(t *testing.T) {
trueValue := true
falseValue := false
type args struct {
d *caddyfile.Dispenser
}
tv := true
fv := false
tests := []struct {
name string
expected *CrowdSec
args args
wantParseErr bool
wantConfigureErr bool
name string
input string
env map[string]string
expected *CrowdSec
wantParseErr bool
}{
{
name: "fail/no-args",
name: "fail/missing tokens",
expected: &CrowdSec{},
input: ``,
wantParseErr: true,
},
{
name: "fail/not-crowdsec",
expected: &CrowdSec{},
input: `not-crowdsec`,
wantParseErr: true,
},
{
name: "fail/invalid-duration",
expected: &CrowdSec{},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec`),
},
wantParseErr: false,
wantConfigureErr: true,
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{},
input: `
crowdsec {
api_url
api_key some_random_key
ticker_interval 30x
}`,
wantParseErr: true,
},
{
name: "fail/invalid-api-url",
expected: &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{},
input: `crowdsec {
api_url example.com
api_key some_random_key
ticker_interval 30x
}`,
wantParseErr: true,
},
{
name: "fail/missing-api-key",
expected: &CrowdSec{},
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key
}`,
wantParseErr: true,
},
{
name: "fail/missing-ticker-interval",
expected: &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{},
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{},
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{},
input: `crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
unknown_token 42
}`,
wantParseErr: true,
},
{
name: "ok/basic",
expected: &CrowdSec{
APIUrl: "http://127.0.0.1:8080/",
APIKey: "some_random_key",
APIUrl: "http://127.0.0.1:8080/",
APIKey: "some_random_key",
TickerInterval: "60s",
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,
wantConfigureErr: false,
}`,
wantParseErr: false,
},
{
name: "ok/full",
expected: &CrowdSec{
APIUrl: "http://127.0.0.1:8080/",
APIKey: "some_random_key",
TickerInterval: "33s",
EnableStreaming: &falseValue,
EnableHardFails: &trueValue,
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,
wantConfigureErr: false,
}`,
wantParseErr: false,
},
{
name: "fail/invalid-duration",
expected: &CrowdSec{},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
ticker_interval 30x
}`),
name: "ok/env-vars",
expected: &CrowdSec{
APIUrl: "http://127.0.0.2:8080/",
APIKey: "env-test-key",
TickerInterval: "25s",
EnableStreaming: &tv,
EnableHardFails: &fv,
},
wantParseErr: true,
wantConfigureErr: false,
},
{
name: "fail/unknown-token",
expected: &CrowdSec{},
args: args{
d: caddyfile.NewTestDispenser(`crowdsec {
api_url http://127.0.0.1:8080
api_key some_random_key
unknown_token 42
}`),
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",
},
wantParseErr: true,
wantConfigureErr: false,
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) {
c := &CrowdSec{}
if _, err := parseCaddyfileGlobalOption(tt.args.d, nil); (err != nil) != tt.wantParseErr {
t.Errorf("CrowdSec.parseCaddyfileGlobalOption() error = %v, wantParseErr %v", err, tt.wantParseErr)
return
for k, v := range tt.env {
t.Setenv(k, v)
}
if err := c.configure(); (err != nil) != tt.wantConfigureErr {
t.Errorf("CrowdSec.configure) error = %v, wantConfigureErr %v", err, tt.wantConfigureErr)
dispenser := caddyfile.NewTestDispenser(tt.input)
jsonApp, err := parseCrowdSec(dispenser, nil)
if tt.wantParseErr {
assert.Error(t, err)
return
}
// TODO: properly use go-cmp and get unexported fields to work
if tt.expected.APIUrl != "" {
if tt.expected.APIUrl != c.APIUrl {
t.Errorf("got: %s, want: %s", c.APIUrl, tt.expected.APIUrl)
}
}
if tt.expected.APIKey != "" {
if tt.expected.APIKey != c.APIKey {
t.Errorf("got: %s, want: %s", c.APIKey, tt.expected.APIKey)
}
}
if tt.expected.TickerInterval != "" {
if tt.expected.TickerInterval != c.TickerInterval {
t.Errorf("got: %s, want: %s", c.TickerInterval, tt.expected.TickerInterval)
}
}
if tt.expected.EnableStreaming != nil {
if *tt.expected.EnableStreaming != *c.EnableStreaming {
t.Errorf("got: %t, want: %t", *c.EnableStreaming, *tt.expected.EnableStreaming)
}
}
if tt.expected.EnableHardFails != nil {
if *tt.expected.EnableHardFails != *c.EnableHardFails {
t.Errorf("got: %t, want: %t", *c.EnableHardFails, *tt.expected.EnableHardFails)
}
}
assert.NoError(t, err)

app, ok := jsonApp.(httpcaddyfile.App)
require.True(t, ok)
assert.Equal(t, "crowdsec", app.Name)

var c CrowdSec
err = json.Unmarshal(app.Value, &c)
require.NoError(t, err)

assert.Equal(t, tt.expected.APIUrl, c.APIUrl)
assert.Equal(t, tt.expected.APIKey, c.APIKey)
assert.Equal(t, tt.expected.TickerInterval, c.TickerInterval)
assert.Equal(t, tt.expected.isStreamingEnabled(), c.isStreamingEnabled())
assert.Equal(t, tt.expected.shouldFailHard(), c.shouldFailHard())
})
}
}
Loading

0 comments on commit 054d8c9

Please sign in to comment.