From 38214094851cb055b1dcf27670318e7a54b6277d Mon Sep 17 00:00:00 2001 From: Christopher Ramey Date: Sun, 14 Nov 2021 07:04:25 -0900 Subject: [PATCH] Switch config to yaml --- config/alarm.go | 14 ++ config/check.go | 14 ++ config/config.go | 93 +++----- config/config_test.go | 57 +++++ config/group.go | 36 ---- config/host.go | 27 +-- config/parser.go | 264 ----------------------- config/testdata/comments-inline.tok | 5 - config/testdata/comments.tok | 6 - config/testdata/dup_api_key.yaml | 9 + config/testdata/dup_group_hosts.yaml | 13 ++ config/testdata/dup_hosts.yaml | 12 ++ config/testdata/missing_api_keyfile.yaml | 8 + config/testdata/no_api_key.yaml | 7 + config/testdata/no_hosts.yaml | 2 + config/testdata/quotes-empty.tok | 3 - config/testdata/quotes-multiline.tok | 6 - config/testdata/quotes.tok | 3 - config/testdata/simple-multiline.tok | 6 - config/testdata/simple-spaces.tok | 1 - config/tokenizer.go | 157 -------------- config/tokenizer_test.go | 83 ------- config/validate.go | 63 ++++++ go.mod | 5 +- go.sum | 4 + 25 files changed, 242 insertions(+), 656 deletions(-) create mode 100644 config/alarm.go create mode 100644 config/check.go create mode 100644 config/config_test.go delete mode 100644 config/group.go delete mode 100644 config/parser.go delete mode 100644 config/testdata/comments-inline.tok delete mode 100644 config/testdata/comments.tok create mode 100644 config/testdata/dup_api_key.yaml create mode 100644 config/testdata/dup_group_hosts.yaml create mode 100644 config/testdata/dup_hosts.yaml create mode 100644 config/testdata/missing_api_keyfile.yaml create mode 100644 config/testdata/no_api_key.yaml create mode 100644 config/testdata/no_hosts.yaml delete mode 100644 config/testdata/quotes-empty.tok delete mode 100644 config/testdata/quotes-multiline.tok delete mode 100644 config/testdata/quotes.tok delete mode 100644 config/testdata/simple-multiline.tok delete mode 100644 config/testdata/simple-spaces.tok delete mode 100644 config/tokenizer.go delete mode 100644 config/tokenizer_test.go create mode 100644 config/validate.go diff --git a/config/alarm.go b/config/alarm.go new file mode 100644 index 0000000..e3c33fc --- /dev/null +++ b/config/alarm.go @@ -0,0 +1,14 @@ +package config + +type Alarm struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Attrs map[string]interface{} `yaml:",inline"` +} + +func (alm Alarm) NameOrType() string { + if alm.Name == "" { + return alm.Type + } + return alm.Name +} diff --git a/config/check.go b/config/check.go new file mode 100644 index 0000000..df6034b --- /dev/null +++ b/config/check.go @@ -0,0 +1,14 @@ +package config + +type Check struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Attrs map[string]interface{} `yaml:",inline"` +} + +func (chk Check) NameOrType() string { + if chk.Name == "" { + return chk.Type + } + return chk.Name +} diff --git a/config/config.go b/config/config.go index 1ab5d4b..5475db3 100644 --- a/config/config.go +++ b/config/config.go @@ -1,89 +1,52 @@ package config import ( - "fmt" - "git.binarythought.com/cdramey/alrm/alarm" + yaml "gopkg.in/yaml.v2" + "io" "os" "time" ) type Config struct { - Groups map[string]*Group - Alarms map[string]alarm.Alarm - Interval time.Duration - DebugLevel int - Listen string - Path string - APIKey []byte - APIKeyFile string + Interval time.Duration `yaml:"interval"` + Listen string `yaml:"listen"` + DebugLevel int `yaml:"debug_level"` + APIKey string `yaml:"api_key"` + APIKeyFile string `yaml:"api_key_file"` + WebRoot string `yaml:"web_root"` + Hosts []Host `yaml:"hosts"` + Alarms []Alarm `yaml:"alarms"` + Groups map[string][]Host `yaml:"groups"` } -func (c *Config) NewAlarm(name string, typename string) (alarm.Alarm, error) { - if c.Alarms == nil { - c.Alarms = make(map[string]alarm.Alarm) - } - - if _, exists := c.Alarms[name]; exists { - return nil, fmt.Errorf("alarm %s already exists", name) +func New() *Config { + return &Config{ + Interval: 30 * time.Second, + Listen: "127.0.0.1:8282", + DebugLevel: 0, + WebRoot: "/", } +} - a, err := alarm.NewAlarm(name, typename) +func ReadConfig(fn string, debuglvl int) (*Config, error) { + f, err := os.Open(fn) if err != nil { return nil, err } - c.Alarms[name] = a - - return a, nil -} - -func (c *Config) NewGroup(name string) (*Group, error) { - if c.Groups == nil { - c.Groups = make(map[string]*Group) - } - - if _, exists := c.Groups[name]; exists { - return nil, fmt.Errorf("group %s already exists", name) - } - - group := &Group{Name: name} - c.Groups[name] = group - return group, nil -} -func (c *Config) SetInterval(val string) error { - interval, err := time.ParseDuration(val) + h, err := io.ReadAll(f) if err != nil { - return err - } - - c.Interval = interval - return nil -} - -func ReadConfig(fn string, debuglvl int) (*Config, error) { - cfg := &Config{ - // Default check interval, 30 seconds - Interval: time.Second * 30, - // Default listen address - Listen: "127.0.0.1:8282", - DebugLevel: debuglvl, - Path: fn, - // API keyfile defaults to alrmrc.key - APIKeyFile: fn + ".key", + return nil, err } - pr := &parser{config: cfg} - if err := pr.parse(); err != nil { + cfg := New() + err = yaml.UnmarshalStrict(h, &cfg) + if err != nil { return nil, err } - - if len(cfg.APIKey) == 0 { - b, err := os.ReadFile(cfg.APIKeyFile) - if err != nil { - return nil, err - } - cfg.APIKey = b + err = cfg.Validate() + if err != nil { + return nil, err } - return cfg, nil } diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..c926f12 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,57 @@ +package config + +import ( + "strings" + "testing" +) + +// Is the absence of hosts and groups detected? +func TestNoHosts(t *testing.T) { + _, err := ReadConfig("testdata/no_hosts.yaml", 0) + if err != ERR_NO_GROUPHOST { + t.Errorf("missing hosts/groups not detected: %s", err.Error()) + } +} + +// Are duplicate host names detected? +func TestDupHosts(t *testing.T) { + _, err := ReadConfig("testdata/dup_hosts.yaml", 0) + if err == nil || !strings.HasPrefix(err.Error(), "duplicate host") { + t.Errorf("duplicate hosts not detected: %s", err.Error()) + } +} + +// Are duplicate host names inside groups detected? +func TestDupGroupHosts(t *testing.T) { + _, err := ReadConfig("testdata/dup_group_hosts.yaml", 0) + if err == nil || !strings.HasPrefix(err.Error(), "duplicate host") { + t.Errorf("duplicate hosts not detected: %s", err.Error()) + } +} + +// Test if api_key and api_key_file are both defined +func TestDupAPI(t *testing.T) { + _, err := ReadConfig("testdata/dup_api_key.yaml", 0) + if err != ERR_DUP_API { + t.Errorf("missing api key/api key file not detected: %s", err.Error()) + } +} + +// Are undefined api key / key files detected? +func TestUndefinedAPI(t *testing.T) { + _, err := ReadConfig("testdata/no_api_key.yaml", 0) + if err != ERR_NO_API { + t.Errorf("undefined api key/api key file not detected: %s", err.Error()) + } +} + +// Are missing api key files detected? +func TestMissingAPIKey(t *testing.T) { + _, err := ReadConfig("testdata/missing_api_keyfile.yaml", 0) + if err != ERR_MISSING_KEYFILE { + t.Errorf("missing api key file not detected: %s", err.Error()) + } +} + +// Test if referenced alarm exists (and is a valid type) +// Test if referenced check exists (and is a valid type) diff --git a/config/group.go b/config/group.go deleted file mode 100644 index f5c4082..0000000 --- a/config/group.go +++ /dev/null @@ -1,36 +0,0 @@ -package config - -import ( - "fmt" -) - -type Group struct { - Name string - Hosts map[string]*Host -} - -func (ag *Group) NewHost(name string) (*Host, error) { - if ag.Hosts == nil { - ag.Hosts = make(map[string]*Host) - } - - if _, exists := ag.Hosts[name]; exists { - return nil, fmt.Errorf("host %s already exists", name) - } - - host := &Host{Name: name} - ag.Hosts[name] = host - return host, nil -} - -func (ag *Group) Check(debuglvl int) error { - for _, host := range ag.Hosts { - for _, chk := range host.Checks { - err := chk.Check(debuglvl) - if err != nil { - return err - } - } - } - return nil -} diff --git a/config/host.go b/config/host.go index 8981686..1f6b234 100644 --- a/config/host.go +++ b/config/host.go @@ -1,27 +1,14 @@ package config -import ( - "git.binarythought.com/cdramey/alrm/check" -) - type Host struct { - Name string - Address string - Checks []check.Check -} - -func (ah *Host) GetAddress() string { - if ah.Address != "" { - return ah.Address - } - return ah.Name + Name string `yaml:"name"` + Address string `yaml:"address"` + Checks []Check `yaml:"checks"` } -func (ah *Host) NewCheck(name string) (check.Check, error) { - chk, err := check.NewCheck(name, ah.GetAddress()) - if err != nil { - return nil, err +func (hst *Host) NameOrAddress() string { + if hst.Name == "" { + return hst.Address } - ah.Checks = append(ah.Checks, chk) - return chk, nil + return hst.Name } diff --git a/config/parser.go b/config/parser.go deleted file mode 100644 index 8040024..0000000 --- a/config/parser.go +++ /dev/null @@ -1,264 +0,0 @@ -package config - -import ( - "fmt" - "git.binarythought.com/cdramey/alrm/alarm" - "git.binarythought.com/cdramey/alrm/check" - "strings" -) - -const ( - PR_NONE = iota - PR_SET - PR_MONITOR - PR_GROUP - PR_HOST - PR_CHECK - PR_ALARM -) - -type parser struct { - config *Config - states []int - lastHost *Host - lastGroup *Group - lastCheck check.Check - lastAlarm alarm.Alarm - lastAlarmName string -} - -func (p *parser) parse() error { - tok, err := NewTokenizer(p.config.Path) - if err != nil { - return err - } - defer tok.Close() - - for tok.Scan() { - tk := tok.Text() - stateswitch: - switch p.state() { - case PR_NONE: - switch strings.ToLower(tk) { - case "monitor": - p.setState(PR_MONITOR) - case "set": - p.setState(PR_SET) - case "alarm": - p.setState(PR_ALARM) - default: - return fmt.Errorf("invalid token in %s, line %d: \"%s\"", - p.config.Path, tok.Line(), tk) - } - - case PR_SET: - key := strings.ToLower(tk) - if !tok.Scan() { - return fmt.Errorf("empty value name for set in %s, line %d", - p.config.Path, tok.Line()) - } - - value := tok.Text() - switch key { - case "interval": - err := p.config.SetInterval(value) - if err != nil { - return fmt.Errorf( - "invalid duration for interval in %s, line %d: \"%s\"", - p.config.Path, tok.Line(), value, - ) - } - case "listen": - p.config.Listen = value - case "api.key": - p.config.APIKey = []byte(value) - case "api.keyfile": - p.config.APIKeyFile = value - default: - return fmt.Errorf("unknown key for set in %s, line %d: \"%s\"", - p.config.Path, tok.Line(), tk, - ) - } - p.prevState() - - case PR_MONITOR: - switch strings.ToLower(tk) { - case "host": - p.setState(PR_HOST) - - case "group": - p.setState(PR_GROUP) - - default: - p.prevState() - goto stateswitch - } - - case PR_GROUP: - if p.lastGroup == nil { - p.lastGroup, err = p.config.NewGroup(tk) - if err != nil { - return fmt.Errorf("%s in %s, line %d", - err.Error(), p.config.Path, tok.Line(), - ) - } - continue - } - - switch strings.ToLower(tk) { - case "host": - p.setState(PR_HOST) - - default: - p.prevState() - goto stateswitch - } - - case PR_HOST: - // If a host has no group, inherit the host name - if p.lastGroup == nil { - p.lastGroup, err = p.config.NewGroup(tk) - if err != nil { - return fmt.Errorf("%s in %s, line %d", - err.Error(), p.config.Path, tok.Line(), - ) - } - } - - if p.lastHost == nil { - p.lastHost, err = p.lastGroup.NewHost(tk) - if err != nil { - return fmt.Errorf("%s in %s, line %d", - err.Error(), p.config.Path, tok.Line(), - ) - } - continue - } - - switch strings.ToLower(tk) { - case "address": - if !tok.Scan() { - return fmt.Errorf("empty address for host in %s, line %d", - p.config.Path, tok.Line()) - } - p.lastHost.Address = tok.Text() - - case "check": - p.setState(PR_CHECK) - - default: - p.prevState() - goto stateswitch - } - - case PR_CHECK: - if p.lastCheck == nil { - p.lastCheck, err = p.lastHost.NewCheck(tk) - if err != nil { - return fmt.Errorf("%s in %s, line %d", - err.Error(), p.config.Path, tok.Line()) - } - continue - } - cont, err := p.lastCheck.Parse(tk) - if err != nil { - return fmt.Errorf("%s in %s, line %d", - err.Error(), p.config.Path, tok.Line()) - } - if !cont { - p.lastCheck = nil - p.prevState() - goto stateswitch - } - - case PR_ALARM: - if p.lastAlarm == nil { - if p.lastAlarmName == "" { - p.lastAlarmName = tk - continue - } - - p.lastAlarm, err = p.config.NewAlarm(p.lastAlarmName, tk) - if err != nil { - return fmt.Errorf("%s in %s, line %d", - err.Error(), p.config.Path, tok.Line()) - } - p.lastAlarmName = "" - continue - } - cont, err := p.lastAlarm.Parse(tk) - if err != nil { - return fmt.Errorf("%s in %s, line %d", - err.Error(), p.config.Path, tok.Line()) - } - if !cont { - p.lastAlarm = nil - p.prevState() - goto stateswitch - } - - default: - return fmt.Errorf("unknown parser state: %d", p.state()) - } - } - if err := tok.Err(); err != nil { - return err - } - return nil -} - -func (p *parser) state() int { - if len(p.states) < 1 { - return PR_NONE - } - return p.states[len(p.states)-1] -} - -func (p *parser) setState(state int) { - switch state { - case PR_SET, PR_MONITOR: - fallthrough - case PR_GROUP: - p.lastGroup = nil - fallthrough - case PR_HOST: - p.lastHost = nil - p.lastCheck = nil - } - - if p.config.DebugLevel > 1 { - fmt.Printf("Parser state: %s", p.stateName()) - } - p.states = append(p.states, state) - if p.config.DebugLevel > 1 { - fmt.Printf(" -> %s\n", p.stateName()) - } -} - -func (p *parser) prevState() int { - if len(p.states) > 0 { - p.states = p.states[:len(p.states)-1] - } - return p.state() -} - -func (p *parser) stateName() string { - switch p.state() { - case PR_NONE: - return "PR_NONE" - case PR_SET: - return "PR_SET" - case PR_MONITOR: - return "PR_MONITOR" - case PR_GROUP: - return "PR_GROUP" - case PR_HOST: - return "PR_HOST" - case PR_CHECK: - return "PR_CHECK" - case PR_ALARM: - return "PR_ALARM" - default: - return "UNKNOWN" - } -} diff --git a/config/testdata/comments-inline.tok b/config/testdata/comments-inline.tok deleted file mode 100644 index 23827b0..0000000 --- a/config/testdata/comments-inline.tok +++ /dev/null @@ -1,5 +0,0 @@ -one #one -"two#three" -# "three" -four -# EOF diff --git a/config/testdata/comments.tok b/config/testdata/comments.tok deleted file mode 100644 index 9f65962..0000000 --- a/config/testdata/comments.tok +++ /dev/null @@ -1,6 +0,0 @@ -# one -one -#two -two - # three - three diff --git a/config/testdata/dup_api_key.yaml b/config/testdata/dup_api_key.yaml new file mode 100644 index 0000000..19de521 --- /dev/null +++ b/config/testdata/dup_api_key.yaml @@ -0,0 +1,9 @@ +interval: 30s +api_key: bogus +api_key_file: bogus.key + +hosts: + - name: localhost + address: 127.0.0.1 + checks: + - type: ping diff --git a/config/testdata/dup_group_hosts.yaml b/config/testdata/dup_group_hosts.yaml new file mode 100644 index 0000000..a2664e4 --- /dev/null +++ b/config/testdata/dup_group_hosts.yaml @@ -0,0 +1,13 @@ +interval: 30s +api_key: bogus + +groups: + servers: + - name: localhost + address: 127.0.0.1 + checks: + - type: ping + - name: localhost + address: 127.0.0.1 + checks: + - type: ping diff --git a/config/testdata/dup_hosts.yaml b/config/testdata/dup_hosts.yaml new file mode 100644 index 0000000..febe158 --- /dev/null +++ b/config/testdata/dup_hosts.yaml @@ -0,0 +1,12 @@ +interval: 30s +api_key: bogus + +hosts: + - name: localhost + address: 127.0.0.1 + checks: + - type: ping + - name: localhost + address: 127.0.0.1 + checks: + - type: ping diff --git a/config/testdata/missing_api_keyfile.yaml b/config/testdata/missing_api_keyfile.yaml new file mode 100644 index 0000000..419e14c --- /dev/null +++ b/config/testdata/missing_api_keyfile.yaml @@ -0,0 +1,8 @@ +interval: 30s +api_key_file: testdata/bogus.key + +hosts: + - name: localhost + address: 127.0.0.1 + checks: + - type: ping diff --git a/config/testdata/no_api_key.yaml b/config/testdata/no_api_key.yaml new file mode 100644 index 0000000..853b26f --- /dev/null +++ b/config/testdata/no_api_key.yaml @@ -0,0 +1,7 @@ +interval: 30s + +hosts: + - name: localhost + address: 127.0.0.1 + checks: + - type: ping diff --git a/config/testdata/no_hosts.yaml b/config/testdata/no_hosts.yaml new file mode 100644 index 0000000..1e67b40 --- /dev/null +++ b/config/testdata/no_hosts.yaml @@ -0,0 +1,2 @@ +interval: 30s +api_key: bogus diff --git a/config/testdata/quotes-empty.tok b/config/testdata/quotes-empty.tok deleted file mode 100644 index 26e6aa0..0000000 --- a/config/testdata/quotes-empty.tok +++ /dev/null @@ -1,3 +0,0 @@ -one "" three -"" five "" -seven diff --git a/config/testdata/quotes-multiline.tok b/config/testdata/quotes-multiline.tok deleted file mode 100644 index 1a74782..0000000 --- a/config/testdata/quotes-multiline.tok +++ /dev/null @@ -1,6 +0,0 @@ -"one -two" 'three -four' - -`five - six` diff --git a/config/testdata/quotes.tok b/config/testdata/quotes.tok deleted file mode 100644 index d2b3977..0000000 --- a/config/testdata/quotes.tok +++ /dev/null @@ -1,3 +0,0 @@ -"one" 'two' `three` - -`four` 'five' "six" diff --git a/config/testdata/simple-multiline.tok b/config/testdata/simple-multiline.tok deleted file mode 100644 index 0b931ed..0000000 --- a/config/testdata/simple-multiline.tok +++ /dev/null @@ -1,6 +0,0 @@ -one two three -four five - - -six - diff --git a/config/testdata/simple-spaces.tok b/config/testdata/simple-spaces.tok deleted file mode 100644 index 9cc4dbe..0000000 --- a/config/testdata/simple-spaces.tok +++ /dev/null @@ -1 +0,0 @@ -one two three four five six diff --git a/config/tokenizer.go b/config/tokenizer.go deleted file mode 100644 index 5277404..0000000 --- a/config/tokenizer.go +++ /dev/null @@ -1,157 +0,0 @@ -package config - -import ( - "bufio" - "fmt" - "io" - "os" - "strings" - "unicode" -) - -const ( - TK_NONE = iota - TK_VAL - TK_QUOTE - TK_COMMENT -) - -type Tokenizer struct { - curline int - repline int - file *os.File - reader *bufio.Reader - text string - err error -} - -func NewTokenizer(fn string) (*Tokenizer, error) { - var err error - tk := &Tokenizer{curline: 1} - tk.file, err = os.Open(fn) - if err != nil { - return nil, err - } - - tk.reader = bufio.NewReader(tk.file) - return tk, nil -} - -func (t *Tokenizer) Close() error { - return t.file.Close() -} - -func (t *Tokenizer) Scan() bool { - t.repline = t.curline - state := TK_NONE - t.text = "" - - var b strings.Builder - var quo rune - for { - var r rune - r, _, t.err = t.reader.ReadRune() - if t.err != nil { - break - } - if r == unicode.ReplacementChar { - t.err = fmt.Errorf("invalid utf-8 encoding on line %s", t.repline) - break - } - - switch state { - case TK_NONE: - // When between values, increment both the reported line - // and the current line, since there's not yet anything - // to report - if r == '\n' { - t.repline++ - t.curline++ - } - - // If we're between values and we encounter a space - // or a control character, ignore it - if unicode.IsSpace(r) || unicode.IsControl(r) { - continue - } - - // If we're between values and we encounter a #, it's - // the beginning of a comment - if r == '#' { - state = TK_COMMENT - continue - } - - // If we're between values and we get a quote character - // treat it as the beginning of a string literal - if r == '"' || r == '\'' || r == '`' { - state = TK_QUOTE - quo = r - continue - } - - b.WriteRune(r) - state = TK_VAL - - case TK_VAL: - // In values, only increment the current line, so - // if an error is reported, it reports the line - // the value starts on - if r == '\n' { - t.curline++ - } - - // If we're in a normal value and we encounter a space - // or a control character, end value - if unicode.IsSpace(r) || unicode.IsControl(r) { - goto end - } - b.WriteRune(r) - - case TK_QUOTE: - // In quotes, only increment the current line, so - // if an error is reported, it reports the line - // the quoted value starts on - if r == '\n' { - t.curline++ - } - - // End this quote if it's another quote of the same rune - if r == quo { - goto end - } - b.WriteRune(r) - - case TK_COMMENT: - // Comments are ignored, until a new line is encounter - // at which point, increment the current and reported line - if r == '\n' { - t.curline++ - t.repline++ - state = TK_NONE - } - continue - } - } - -end: - if t.err == nil || t.err == io.EOF { - t.text = b.String() - } - return t.err == nil -} - -func (t *Tokenizer) Text() string { - return t.text -} - -func (t *Tokenizer) Line() int { - return t.repline -} - -func (t *Tokenizer) Err() error { - if t.err == io.EOF { - return nil - } - return t.err -} diff --git a/config/tokenizer_test.go b/config/tokenizer_test.go deleted file mode 100644 index 8c8db40..0000000 --- a/config/tokenizer_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package config - -import ( - "encoding/json" - "testing" -) - -func TestSimpleSpaces(t *testing.T) { - runTest(t, "simple-spaces", - `[["one","two","three","four","five","six"]]`, - ) -} - -func TestSimpleMultiline(t *testing.T) { - runTest(t, "simple-multiline", - `[["one","two","three"],["four","five"],[],[],["six"]]`, - ) -} - -func TestQuotes(t *testing.T) { - runTest(t, "quotes", - `[["one","two","three"],[],["four","five","six"]]`, - ) -} - -func TestQuotesMultiline(t *testing.T) { - runTest(t, "quotes-multiline", - `[["one\ntwo"],["three\nfour"],[],[],["five\n six"]]`, - ) -} - -func TestQuotesEmpty(t *testing.T) { - runTest(t, "quotes-empty", - `[["one","","three"],["","five",""],["seven"]]`, - ) -} - -func TestComments(t *testing.T) { - runTest(t, "comments", - `[[],["one"],[],["two"],[],["three"]]`, - ) -} - -func TestCommentsInline(t *testing.T) { - runTest(t, "comments-inline", - `[["one"],["two#three"],[],["four"]]`, - ) -} - -func runTest(t *testing.T, bn string, exp string) { - t.Logf("Running testdata/%s.tok.. ", bn) - tok, err := NewTokenizer("testdata/" + bn + ".tok") - if err != nil { - t.Fatalf("%s", err.Error()) - } - defer tok.Close() - - tokens := [][]string{} - for tok.Scan() { - ln := tok.Line() - tl := len(tokens) - if tl < ln { - for i := tl; i < ln; i++ { - tokens = append(tokens, []string{}) - } - } - tokens[ln-1] = append(tokens[ln-1], tok.Text()) - } - if tok.Err() != nil { - t.Fatalf("%s", tok.Err()) - } - - out, err := json.Marshal(tokens) - if err != nil { - t.Fatalf("%s", err) - } - - if exp != string(out) { - t.Logf("Expected: %s", exp) - t.Logf("Got: %s", out) - t.FailNow() - } -} diff --git a/config/validate.go b/config/validate.go new file mode 100644 index 0000000..95b1ca8 --- /dev/null +++ b/config/validate.go @@ -0,0 +1,63 @@ +package config + +import ( + "fmt" + "os" +) + +var ERR_DUP_API error = fmt.Errorf( + "api key and api key file cannot both be defined", +) +var ERR_NO_API error = fmt.Errorf( + "no api key or api key file defined", +) +var ERR_NO_GROUPHOST error = fmt.Errorf( + "no groups or hosts configured", +) +var ERR_MISSING_KEYFILE error = fmt.Errorf( + "api key file not found", +) + +func (cfg *Config) Validate() error { + if cfg.APIKey != "" && cfg.APIKeyFile != "" { + return ERR_DUP_API + } + + if cfg.APIKey == "" && cfg.APIKeyFile == "" { + return ERR_NO_API + } + + if cfg.APIKeyFile != "" { + if _, err := os.Stat(cfg.APIKeyFile); err != nil { + return ERR_MISSING_KEYFILE + } + } + + if len(cfg.Groups) == 0 && len(cfg.Hosts) == 0 { + return ERR_NO_GROUPHOST + } + + set := make(map[string]bool) + for _, host := range cfg.Hosts { + name := host.NameOrAddress() + if _, exists := set[name]; exists { + return fmt.Errorf("duplicate host \"%s\"", name) + } + set[name] = true + } + + for group, hosts := range cfg.Groups { + set = make(map[string]bool) + for _, host := range hosts { + name := host.NameOrAddress() + if _, exists := set[name]; exists { + return fmt.Errorf( + "duplicate host \"%s\" in group \"%s\"", group, name, + ) + } + set[name] = true + } + } + + return nil +} diff --git a/go.mod b/go.mod index 69614ad..e057b72 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module git.binarythought.com/cdramey/alrm go 1.16 -require golang.org/x/net v0.0.0-20201224014010-6772e930b67b +require ( + golang.org/x/net v0.0.0-20201224014010-6772e930b67b + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum index 89bc358..6b73a50 100644 --- a/go.sum +++ b/go.sum @@ -5,3 +5,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=