-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
YAML & JSON configuration handling added --------- Signed-off-by: Igor Shishkin <me@teran.dev>
- Loading branch information
Showing
9 changed files
with
424 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package config | ||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/pkg/errors" | ||
yaml "gopkg.in/yaml.v3" | ||
) | ||
|
||
type Check struct { | ||
Kind string `json:"kind"` | ||
Spec json.RawMessage `json:"spec"` | ||
} | ||
|
||
type Peer struct { | ||
Name string `json:"name"` | ||
RemoteAddress string `json:"remote_address"` | ||
RemoteAS uint64 `json:"remote_as"` | ||
LocalAddress string `json:"local_address"` | ||
LocalAS uint64 `json:"local_as"` | ||
Routes []string `json:"routes"` | ||
} | ||
|
||
type Service struct { | ||
Name string `json:"name"` | ||
CheckOperator string `json:"check_operator"` | ||
CheckInterval Duration `json:"check_interval"` | ||
Checks []Check `json:"checks"` | ||
Peers []Peer `json:"peers"` | ||
} | ||
|
||
type Metrics struct { | ||
Enabled bool `json:"enabled"` | ||
Address string `json:"address"` | ||
} | ||
|
||
type Config struct { | ||
Services []Service `json:"services"` | ||
Metrics Metrics `json:"metrics"` | ||
} | ||
|
||
func NewFromFile(filename string) (*Config, error) { | ||
cfg := &Config{} | ||
|
||
data, err := os.ReadFile(filename) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error reading configuration file") | ||
} | ||
|
||
ext := strings.ToLower(filepath.Ext(filename)) | ||
|
||
switch ext { | ||
case ".yml", ".yaml": | ||
type intermediate struct { | ||
Services []any `yaml:"services"` | ||
Metrics map[string]any `yaml:"metrics"` | ||
} | ||
|
||
d := intermediate{} | ||
|
||
err := yaml.Unmarshal(data, &d) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error unmarshaling intermediate configuration") | ||
} | ||
|
||
data, err = json.Marshal(d) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error marshaling intermediate configuration") | ||
} | ||
case ".json": | ||
// no additional action needed | ||
default: | ||
return nil, errors.Errorf("unexpected file format: `%s`", ext) | ||
} | ||
|
||
if err := json.Unmarshal(data, &cfg); err != nil { | ||
return nil, errors.Wrap(err, "error unmarshaling config") | ||
} | ||
|
||
return cfg, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package config | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
"time" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestConfig(t *testing.T) { | ||
type testCase struct { | ||
name string | ||
samplePath string | ||
expOut Config | ||
expError error | ||
} | ||
|
||
sampleConfig := Config{ | ||
Services: []Service{ | ||
{ | ||
Name: "http", | ||
CheckOperator: "and", | ||
CheckInterval: Duration(time.Duration(10 * time.Second)), | ||
Checks: []Check{ | ||
{ | ||
Kind: "dns_lookup", | ||
Spec: json.RawMessage(`{"interval":"100ms","query":"example.com","resolver":"127.0.0.1","tries":3}`), | ||
}, | ||
{ | ||
Kind: "http_2xx", | ||
Spec: json.RawMessage(`{"address":"127.0.0.1:8080","interval":"100ms","path":"/","timeout":"2s","tries":3}`), | ||
}, | ||
{ | ||
Kind: "assigned_address", | ||
Spec: json.RawMessage(`{"interface":"dummy0","ipv4":"10.0.0.128"}`), | ||
}, | ||
}, | ||
Peers: []Peer{ | ||
{ | ||
Name: "some_router_1", | ||
RemoteAddress: "10.0.0.252", | ||
RemoteAS: 65000, | ||
LocalAddress: "10.0.0.1", | ||
LocalAS: 65999, | ||
Routes: []string{"10.0.0.128/32"}, | ||
}, | ||
{ | ||
Name: "some_router_2", | ||
RemoteAddress: "10.0.0.253", | ||
RemoteAS: 65000, | ||
LocalAddress: "10.0.0.1", | ||
LocalAS: 65999, | ||
Routes: []string{"10.0.0.128/32"}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
Metrics: Metrics{ | ||
Enabled: true, | ||
Address: "127.0.0.1:9090", | ||
}, | ||
} | ||
|
||
tcs := []testCase{ | ||
{ | ||
name: "YAML configuration", | ||
samplePath: "testdata/sample.yaml", | ||
expOut: sampleConfig, | ||
}, | ||
{ | ||
name: "JSON configuration", | ||
samplePath: "testdata/sample.json", | ||
expOut: sampleConfig, | ||
}, | ||
{ | ||
name: "wrong config file extension", | ||
samplePath: "testdata/sample.unknown", | ||
expError: errors.New("unexpected file format: `.unknown`"), | ||
}, | ||
} | ||
|
||
for _, tc := range tcs { | ||
t.Run(tc.name, func(t *testing.T) { | ||
r := require.New(t) | ||
|
||
cfg, err := NewFromFile(tc.samplePath) | ||
if tc.expError == nil { | ||
r.NoError(err) | ||
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) | ||
} | ||
r.Equalf(tc.expOut.Services[i].Peers, cfg.Services[i].Peers, "svc#%d", i) | ||
} | ||
r.Equal(tc.expOut.Metrics, cfg.Metrics) | ||
} else { | ||
r.Error(err) | ||
r.Equal(tc.expError.Error(), err.Error()) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package config | ||
|
||
import ( | ||
"encoding/json" | ||
"time" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
type Duration time.Duration | ||
|
||
func (d Duration) MarshalJSON() ([]byte, error) { | ||
return json.Marshal(time.Duration(d).String()) | ||
} | ||
|
||
func (d *Duration) UnmarshalJSON(b []byte) error { | ||
var v any | ||
|
||
if err := json.Unmarshal(b, &v); err != nil { | ||
return err | ||
} | ||
|
||
switch value := v.(type) { | ||
case float64: | ||
*d = Duration(time.Duration(value)) | ||
return nil | ||
case string: | ||
tmp, err := time.ParseDuration(value) | ||
if err != nil { | ||
return err | ||
} | ||
*d = Duration(tmp) | ||
return nil | ||
default: | ||
return errors.Errorf("invalid value: `%v` (%T)", value, value) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package config | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
"time" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestDuration(t *testing.T) { | ||
type testCase struct { | ||
name string | ||
in string | ||
expOut Duration | ||
expError error | ||
} | ||
tcs := []testCase{ | ||
{ | ||
name: "number", | ||
in: "10", | ||
expOut: Duration(time.Duration(10)), | ||
}, | ||
{ | ||
name: "number w/ suffix", | ||
in: `"10s"`, | ||
expOut: Duration(time.Duration(10 * time.Second)), | ||
}, | ||
{ | ||
name: "invalid string", | ||
in: `"blah"`, | ||
expError: errors.Errorf(`time: invalid duration "blah"`), | ||
}, | ||
{ | ||
name: "boolean", | ||
in: "true", | ||
expError: errors.Errorf("invalid value: `true` (bool)"), | ||
}, | ||
} | ||
|
||
for _, tc := range tcs { | ||
t.Run(tc.name, func(t *testing.T) { | ||
r := require.New(t) | ||
|
||
v := new(Duration) | ||
err := json.Unmarshal([]byte(tc.in), v) | ||
if tc.expError == nil { | ||
r.NoError(err) | ||
r.Equal(&tc.expOut, v) | ||
} else { | ||
r.Error(err) | ||
r.Equal(tc.expError.Error(), err.Error()) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
{ | ||
"services": [ | ||
{ | ||
"name": "http", | ||
"check_operator": "and", | ||
"check_interval": "10s", | ||
"checks": [ | ||
{ | ||
"kind": "dns_lookup", | ||
"spec": { | ||
"query": "example.com", | ||
"resolver": "127.0.0.1", | ||
"tries": 3, | ||
"interval": "100ms" | ||
} | ||
}, | ||
{ | ||
"kind": "http_2xx", | ||
"spec": { | ||
"address": "127.0.0.1:8080", | ||
"path": "/", | ||
"tries": 3, | ||
"interval": "100ms", | ||
"timeout": "2s" | ||
} | ||
}, | ||
{ | ||
"kind": "assigned_address", | ||
"spec": { | ||
"interface": "dummy0", | ||
"ipv4": "10.0.0.128" | ||
} | ||
} | ||
], | ||
"peers": [ | ||
{ | ||
"name": "some_router_1", | ||
"remote_address": "10.0.0.252", | ||
"remote_as": 65000, | ||
"local_address": "10.0.0.1", | ||
"local_as": 65999, | ||
"routes": [ | ||
"10.0.0.128/32" | ||
] | ||
}, | ||
{ | ||
"name": "some_router_2", | ||
"remote_address": "10.0.0.253", | ||
"remote_as": 65000, | ||
"local_address": "10.0.0.1", | ||
"local_as": 65999, | ||
"routes": [ | ||
"10.0.0.128/32" | ||
] | ||
} | ||
] | ||
} | ||
], | ||
"metrics": { | ||
"enabled": true, | ||
"address": "127.0.0.1:9090" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
nothing here |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
--- | ||
services: | ||
- name: http | ||
check_operator: and | ||
check_interval: 10s | ||
checks: | ||
- kind: dns_lookup | ||
spec: | ||
query: example.com | ||
resolver: 127.0.0.1 | ||
tries: 3 | ||
interval: 100ms | ||
- kind: http_2xx | ||
spec: | ||
address: 127.0.0.1:8080 | ||
path: / | ||
tries: 3 | ||
interval: 100ms | ||
timeout: 2s | ||
- kind: assigned_address | ||
spec: | ||
interface: dummy0 | ||
ipv4: 10.0.0.128 | ||
peers: | ||
- name: some_router_1 | ||
remote_address: 10.0.0.252 | ||
remote_as: 65000 | ||
local_address: 10.0.0.1 | ||
local_as: 65999 | ||
routes: | ||
- 10.0.0.128/32 | ||
- name: some_router_2 | ||
remote_address: 10.0.0.253 | ||
remote_as: 65000 | ||
local_address: 10.0.0.1 | ||
local_as: 65999 | ||
routes: | ||
- 10.0.0.128/32 | ||
metrics: | ||
enabled: true | ||
address: 127.0.0.1:9090 |
Oops, something went wrong.