Skip to content

Commit

Permalink
Extend configuration validation (#28)
Browse files Browse the repository at this point in the history
Add configuration validation to report any issues on earlier stages,
right at start actually.

Signed-off-by: Igor Shishkin <me@teran.dev>
  • Loading branch information
teran authored Jun 5, 2024
1 parent 6faa85e commit 356c2b7
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 6 deletions.
65 changes: 63 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,24 @@ import (
"path/filepath"
"strings"

"github.com/asaskevich/govalidator"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v3"
)

var isCIDR = validation.NewStringRuleWithError(govalidator.IsCIDR, validation.NewError("validation_is_cidr", "must be a valid CIDR"))

var (
_ validation.Validatable = (*Config)(nil)
_ validation.Validatable = (*Announcer)(nil)
_ validation.Validatable = (*Service)(nil)
_ validation.Validatable = (*Metrics)(nil)
_ validation.Validatable = (*Check)(nil)
_ validation.Validatable = (*Peer)(nil)
)

type Announcer struct {
RouterID string `json:"router_id"`
LocalAddress string `json:"local_address"`
Expand All @@ -18,35 +32,82 @@ type Announcer struct {
Peers []Peer `json:"peers"`
}

func (a Announcer) Validate() error {
return validation.ValidateStruct(&a,
validation.Field(&a.RouterID, validation.Required, is.IPv4),
validation.Field(&a.LocalAddress, validation.Required, is.IPv4),
validation.Field(&a.LocalASN, validation.Required),
validation.Field(&a.Routes, validation.Required, validation.Each(isCIDR)),
validation.Field(&a.Peers, validation.Required),
)
}

type Check struct {
Kind string `json:"kind"`
Spec json.RawMessage `json:"spec"`
}

func (c Check) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Kind, validation.Required),
validation.Field(&c.Spec, validation.Required),
)
}

type Peer struct {
Name string `json:"name"`
RemoteAddress string `json:"remote_address"`
RemoteASN uint32 `json:"remote_asn"`
}

func (p Peer) Validate() error {
return validation.ValidateStruct(&p,
validation.Field(&p.Name, validation.Required),
validation.Field(&p.RemoteAddress, validation.Required, is.IPv4),
validation.Field(&p.RemoteASN, validation.Required),
)
}

type Service struct {
Name string `json:"name"`
CheckOperator string `json:"check_operator"`
CheckInterval Duration `json:"check_interval"`
Checks []Check `json:"checks"`
}

func (s Service) Validate() error {
return validation.ValidateStruct(&s,
validation.Field(&s.Name, validation.Required),
validation.Field(&s.CheckInterval, validation.Required),
validation.Field(&s.Checks, validation.Required),
)
}

type Metrics struct {
Enabled bool `json:"enabled"`
Address string `json:"address"`
}

func (m Metrics) Validate() error {
return validation.ValidateStruct(&m,
validation.Field(&m.Enabled, validation.Required),
validation.Field(&m.Address, validation.Required),
)
}

type Config struct {
Announcer Announcer `json:"announcer"`
Services []Service `json:"services"`
Metrics Metrics `json:"metrics"`
}

func (c *Config) Validate() error {
return validation.ValidateStruct(c,
validation.Field(&c.Announcer, validation.Required),
validation.Field(&c.Services, validation.Required),
validation.Field(&c.Metrics, validation.Required),
)
}

func NewFromFile(filename string) (*Config, error) {
cfg := &Config{}

Expand Down Expand Up @@ -86,5 +147,5 @@ func NewFromFile(filename string) (*Config, error) {
return nil, errors.Wrap(err, "error unmarshaling config")
}

return cfg, nil
return cfg, cfg.Validate()
}
21 changes: 19 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func TestConfig(t *testing.T) {
Services: []Service{
{
Name: "http",
CheckOperator: "and",
CheckInterval: Duration(time.Duration(10 * time.Second)),
Checks: []Check{
{
Expand Down Expand Up @@ -92,7 +91,6 @@ func TestConfig(t *testing.T) {
for i := range tc.expOut.Services {
r.Equalf(tc.expOut.Services[i].Name, cfg.Services[i].Name, "svc#%d", i)
r.Equalf(tc.expOut.Services[i].CheckInterval, cfg.Services[i].CheckInterval, "svc#%d", i)
r.Equalf(tc.expOut.Services[i].CheckOperator, cfg.Services[i].CheckOperator, "svc#%d", i)
for j := range tc.expOut.Services[i].Checks {
r.Equalf(tc.expOut.Services[i].Checks[j].Kind, cfg.Services[i].Checks[j].Kind, "svc#%d check#%d", i, j)
r.JSONEqf(string(tc.expOut.Services[i].Checks[j].Spec), string(cfg.Services[i].Checks[j].Spec), "svc#%d check#%d", i, j)
Expand All @@ -106,3 +104,22 @@ func TestConfig(t *testing.T) {
})
}
}

func TestEmptyConfig(t *testing.T) {
r := require.New(t)

_, err := NewFromFile("testdata/empty.yaml")
r.Error(err)
r.Equal(
"announcer: (local_address: cannot be blank; local_asn: cannot be blank; peers: cannot be blank; router_id: cannot be blank; routes: cannot be blank.); metrics: (address: cannot be blank; enabled: cannot be blank.); services: cannot be blank.",
err.Error(),
)
}

func TestEmptyAnnouncerOnlyConfig(t *testing.T) {
r := require.New(t)

// Actually there are no semantics checks so it should pass
_, err := NewFromFile("testdata/empty_with_announcer.yaml")
r.NoError(err)
}
1 change: 1 addition & 0 deletions config/testdata/empty.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
---
23 changes: 23 additions & 0 deletions config/testdata/empty_with_announcer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
announcer:
router_id: 10.3.3.3
local_address: 10.0.0.1
local_asn: 65999
routes:
- 10.0.0.128/32
peers:
- name: some_router_1
remote_address: 10.0.0.252
remote_asn: 65000
- name: some_router_2
remote_address: 10.0.0.253
remote_asn: 65000
services:
- name: http
check_interval: 10s
checks:
- kind: dns_lookup
spec: {}
metrics:
enabled: true
address: address
1 change: 0 additions & 1 deletion config/testdata/sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"services": [
{
"name": "http",
"check_operator": "and",
"check_interval": "10s",
"checks": [
{
Expand Down
1 change: 0 additions & 1 deletion config/testdata/sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ announcer:
remote_asn: 65000
services:
- name: http
check_operator: and
check_interval: 10s
checks:
- kind: dns_lookup
Expand Down

0 comments on commit 356c2b7

Please sign in to comment.