diff --git a/docs/manual.md b/docs/manual.md index 3460fa8c0..ecfb46872 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -24,6 +24,7 @@ * [interface](#interface) * [kernel-param](#kernel-param) * [mount](#mount) + * [matching](#matching) * [package](#package) * [port](#port) * [process](#process) @@ -614,6 +615,63 @@ mount: filesystem: xfs ``` +### matching +Validates specified content against a matcher. Best used with [Templates](#templates). + +#### With [Templates](#templates): +Let's say we have a `data.json` file that gets generated as part of some testing pipeline: + +```json +{ + "instance_count": 14, + "failures": 3, + "status": "FAIL" +} +``` + +This could then be passed into goss: `goss --vars data.json validate` + +And then validated against: + +```yaml +matching: + check_instance_count: # Make sure there is at least one instance + content: {{ .Vars.instance_count }} + matches: + gt: 0 + + check_failure_count_from_all_instance: # expect no failures + content: {{ .Vars.failures }} + matches: 0 + + check_status: + content: {{ .Vars.status }} + matches: + - not: FAIL +``` + +#### Without [Templates](#templates): +```yaml +matching: + has_substr: # friendly test name + content: some string + matches: + match-regexp: some str + has_2: + content: + - 2 + matches: + contain-element: 2 + has_foo_bar_and_baz: + content: + foo: bar + baz: bing + matches: + and: + - have-key-with-value: + foo: bar + - have-key: baz +``` ### package Validates the state of a package diff --git a/goss_config.go b/goss_config.go index 9363b2991..9e28ece1b 100644 --- a/goss_config.go +++ b/goss_config.go @@ -22,6 +22,7 @@ type GossConfig struct { Mounts resource.MountMap `json:"mount,omitempty" yaml:"mount,omitempty"` Interfaces resource.InterfaceMap `json:"interface,omitempty" yaml:"interface,omitempty"` HTTPs resource.HTTPMap `json:"http,omitempty" yaml:"http,omitempty"` + Matchings resource.MatchingMap `json:"matching,omitempty" yaml:"matching,omitempty"` } func NewGossConfig() *GossConfig { @@ -47,7 +48,23 @@ func NewGossConfig() *GossConfig { func (c *GossConfig) Resources() []resource.Resource { var tests []resource.Resource - gm := genericConcatMaps(c.Commands, c.HTTPs, c.Addrs, c.DNS, c.Packages, c.Services, c.Files, c.Processes, c.Users, c.Groups, c.Ports, c.KernelParams, c.Mounts, c.Interfaces) + gm := genericConcatMaps(c.Commands, + c.HTTPs, + c.Addrs, + c.DNS, + c.Packages, + c.Services, + c.Files, + c.Processes, + c.Users, + c.Groups, + c.Ports, + c.KernelParams, + c.Mounts, + c.Interfaces, + c.Matchings, + ) + for _, m := range gm { for _, t := range m { // FIXME: Can this be moved to a safer compile-time check? diff --git a/resource/matching.go b/resource/matching.go new file mode 100644 index 000000000..0cf2f6cd0 --- /dev/null +++ b/resource/matching.go @@ -0,0 +1,107 @@ +package resource + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/aelsabbahy/goss/system" + "github.com/aelsabbahy/goss/util" +) + +type Matching struct { + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + Content interface{} `json:"content,omitempty" yaml:"content,omitempty"` + Id string `json:"-" yaml:"-"` + Matches matcher `json:"matches" yaml:"matches"` +} + +type MatchingMap map[string]*Matching + +func (a *Matching) ID() string { return a.Id } +func (a *Matching) SetID(id string) { a.Id = id } + +// FIXME: Can this be refactored? +func (r *Matching) GetTitle() string { return r.Title } +func (r *Matching) GetMeta() meta { return r.Meta } + +func (a *Matching) Validate(sys system.System) []TestResult { + skip := false + + // ValidateValue expects a function + stub := func() (interface{}, error) { + return a.Content, nil + } + + var results []TestResult + results = append(results, ValidateValue(a, "matches", a.Matches, stub, skip)) + return results +} + +func (ret *MatchingMap) UnmarshalJSON(data []byte) error { + // Curried json.Unmarshal + unmarshal := func(i interface{}) error { + if err := json.Unmarshal(data, i); err != nil { + return err + } + return nil + } + + // Validate configuration + zero := Matching{} + whitelist, err := util.WhitelistAttrs(zero, util.JSON) + if err != nil { + return err + } + if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { + return err + } + + var tmp map[string]*Matching + if err := unmarshal(&tmp); err != nil { + return err + } + + typ := reflect.TypeOf(zero) + typs := strings.Split(typ.String(), ".")[1] + for id, res := range tmp { + if res == nil { + return fmt.Errorf("Could not parse resource %s:%s", typs, id) + } + res.SetID(id) + } + + *ret = tmp + return nil +} + +func (ret *MatchingMap) UnmarshalYAML(unmarshal func(v interface{}) error) error { + // Validate configuration + zero := Matching{} + whitelist, err := util.WhitelistAttrs(zero, util.YAML) + if err != nil { + return err + } + if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { + return err + } + + var tmp map[string]*Matching + if err := unmarshal(&tmp); err != nil { + return err + } + + typ := reflect.TypeOf(zero) + typs := strings.Split(typ.String(), ".")[1] + for id, res := range tmp { + if res == nil { + return fmt.Errorf("Could not parse resource %s:%s", typs, id) + } + res.SetID(id) + } + + *ret = tmp + return nil +}