diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb1bfb..8ba5034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # v0.6.0 - Fixed a bug where DNS record lookups never were fired wihout a nameserver + - Removed global `--package` option, added `package-manager` property to `package` resource + - Removed code dependency on `*cli.Context` # v0.5.0 diff --git a/add.go b/add.go index b7951f6..641144e 100644 --- a/add.go +++ b/add.go @@ -2,29 +2,42 @@ package goss import ( "fmt" + "github.com/SimonBaeumer/goss/resource" + "github.com/SimonBaeumer/goss/system" + "github.com/SimonBaeumer/goss/util" + "io" "os" + "reflect" "strconv" "strings" - "time" - - "github.com/SimonBaeumer/goss/system" - "github.com/SimonBaeumer/goss/util" - "github.com/urfave/cli" ) +type Add struct { + Writer io.Writer + ExcludeAttr []string + Timeout int + AllowInsecure bool + NoFollowRedirects bool + Server string + Username string + Password string + Header string + Sys *system.System +} + // AddResources is a sSimple wrapper to add multiple resources -func AddResources(fileName, resourceName string, keys []string, c *cli.Context) error { +func (a *Add) AddResources(fileName, resourceName string, keys []string) error { OutStoreFormat = getStoreFormatFromFileName(fileName) - header := extractHeaderArgument(c.String("header")) + header := extractHeaderArgument(a.Header) config := util.Config{ - IgnoreList: c.GlobalStringSlice("exclude-attr"), - Timeout: int(c.Duration("timeout") / time.Millisecond), - AllowInsecure: c.Bool("insecure"), - NoFollowRedirects: c.Bool("no-follow-redirects"), - Server: c.String("server"), - Username: c.String("username"), - Password: c.String("password"), + IgnoreList: a.ExcludeAttr, + Timeout: a.Timeout, + AllowInsecure: a.AllowInsecure, + NoFollowRedirects: a.NoFollowRedirects, + Server: a.Server, + Username: a.Username, + Password: a.Password, Header: header, } @@ -35,10 +48,8 @@ func AddResources(fileName, resourceName string, keys []string, c *cli.Context) gossConfig = *NewGossConfig() } - sys := system.New(c) - for _, key := range keys { - if err := AddResource(fileName, gossConfig, resourceName, key, c, config, sys); err != nil { + if err := a.AddResource(fileName, gossConfig, resourceName, key, config); err != nil { return err } } @@ -58,114 +69,114 @@ func extractHeaderArgument(headerArg string) map[string][]string { } // AddResource adds a resource to the configuration file -func AddResource(fileName string, gossConfig GossConfig, resourceName, key string, c *cli.Context, config util.Config, sys *system.System) error { +func (a *Add) AddResource(fileName string, gossConfig GossConfig, resourceName, key string, config util.Config) error { // Need to figure out a good way to refactor this switch resourceName { - case "Addr": - res, err := gossConfig.Addrs.AppendSysResource(key, sys, config) + case "addr": + res, err := gossConfig.Addrs.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "Command": - res, err := gossConfig.Commands.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "command": + res, err := gossConfig.Commands.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "DNS": - res, err := gossConfig.DNS.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "dns": + res, err := gossConfig.DNS.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "File": - res, err := gossConfig.Files.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "file": + res, err := gossConfig.Files.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) + a.resourcePrint(fileName, res) case "Group": - res, err := gossConfig.Groups.AppendSysResource(key, sys, config) + res, err := gossConfig.Groups.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "Package": - res, err := gossConfig.Packages.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "package": + res, err := gossConfig.Packages.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "Port": - res, err := gossConfig.Ports.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "port": + res, err := gossConfig.Ports.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "Process": - res, err := gossConfig.Processes.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "process": + res, err := gossConfig.Processes.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "Service": - res, err := gossConfig.Services.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "service": + res, err := gossConfig.Services.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "User": - res, err := gossConfig.Users.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "user": + res, err := gossConfig.Users.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "Gossfile": - res, err := gossConfig.Gossfiles.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "gossfile": + res, err := gossConfig.Gossfiles.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "KernelParam": - res, err := gossConfig.KernelParams.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "kernel-param": + res, err := gossConfig.KernelParams.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) + a.resourcePrint(fileName, res) case "Mount": - res, err := gossConfig.Mounts.AppendSysResource(key, sys, config) + res, err := gossConfig.Mounts.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "Interface": - res, err := gossConfig.Interfaces.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "interface": + res, err := gossConfig.Interfaces.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) - case "HTTP": - res, err := gossConfig.HTTPs.AppendSysResource(key, sys, config) + a.resourcePrint(fileName, res) + case "http": + res, err := gossConfig.HTTPs.AppendSysResource(key, a.Sys, config) if err != nil { fmt.Println(err) os.Exit(1) } - resourcePrint(fileName, res) + a.resourcePrint(fileName, res) default: panic("Undefined resource name: " + resourceName) } @@ -174,12 +185,8 @@ func AddResource(fileName string, gossConfig GossConfig, resourceName, key strin } // Simple wrapper to add multiple resources -func AutoAddResources(fileName string, keys []string, c *cli.Context) error { +func (a *Add) AutoAddResources(fileName string, keys []string) error { OutStoreFormat = getStoreFormatFromFileName(fileName) - config := util.Config{ - IgnoreList: c.GlobalStringSlice("exclude-attr"), - Timeout: int(c.Duration("timeout") / time.Millisecond), - } var gossConfig GossConfig if _, err := os.Stat(fileName); err == nil { @@ -188,10 +195,8 @@ func AutoAddResources(fileName string, keys []string, c *cli.Context) error { gossConfig = *NewGossConfig() } - sys := system.New(c) - for _, key := range keys { - if err := AutoAddResource(fileName, gossConfig, key, c, config, sys); err != nil { + if err := a.AutoAddResource(fileName, gossConfig, key); err != nil { return err } } @@ -203,32 +208,32 @@ func AutoAddResources(fileName string, keys []string, c *cli.Context) error { } // Autoadds all resources to the config file -func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *cli.Context, config util.Config, sys *system.System) error { +func (a *Add) AutoAddResource(fileName string, gossConfig GossConfig, key string) error { // file if strings.Contains(key, "/") { - if res, _, ok := gossConfig.Files.AppendSysResourceIfExists(key, sys); ok == true { - resourcePrint(fileName, res) + if res, _, ok := gossConfig.Files.AppendSysResourceIfExists(key, a.Sys); ok == true { + a.resourcePrint(fileName, res) } } // group - if res, _, ok := gossConfig.Groups.AppendSysResourceIfExists(key, sys); ok == true { - resourcePrint(fileName, res) + if res, _, ok := gossConfig.Groups.AppendSysResourceIfExists(key, a.Sys); ok == true { + a.resourcePrint(fileName, res) } // package - if res, _, ok := gossConfig.Packages.AppendSysResourceIfExists(key, sys); ok == true { - resourcePrint(fileName, res) + if res, _, ok := gossConfig.Packages.AppendSysResourceIfExists(key, a.Sys); ok == true { + a.resourcePrint(fileName, res) } // port - if res, _, ok := gossConfig.Ports.AppendSysResourceIfExists(key, sys); ok == true { - resourcePrint(fileName, res) + if res, _, ok := gossConfig.Ports.AppendSysResourceIfExists(key, a.Sys); ok == true { + a.resourcePrint(fileName, res) } // process - if res, sysres, ok := gossConfig.Processes.AppendSysResourceIfExists(key, sys); ok == true { - resourcePrint(fileName, res) + if res, sysres, ok := gossConfig.Processes.AppendSysResourceIfExists(key, a.Sys); ok == true { + a.resourcePrint(fileName, res) ports := system.GetPorts(true) pids, _ := sysres.Pids() for _, pid := range pids { @@ -237,8 +242,8 @@ func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *cli. for _, entry := range entries { if entry.Pid == pidS { // port - if res, _, ok := gossConfig.Ports.AppendSysResourceIfExists(port, sys); ok == true { - resourcePrint(fileName, res) + if res, _, ok := gossConfig.Ports.AppendSysResourceIfExists(port, a.Sys); ok == true { + a.resourcePrint(fileName, res) } } } @@ -247,14 +252,24 @@ func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *cli. } // Service - if res, _, ok := gossConfig.Services.AppendSysResourceIfExists(key, sys); ok == true { - resourcePrint(fileName, res) + if res, _, ok := gossConfig.Services.AppendSysResourceIfExists(key, a.Sys); ok == true { + a.resourcePrint(fileName, res) } // user - if res, _, ok := gossConfig.Users.AppendSysResourceIfExists(key, sys); ok == true { - resourcePrint(fileName, res) + if res, _, ok := gossConfig.Users.AppendSysResourceIfExists(key, a.Sys); ok == true { + a.resourcePrint(fileName, res) } return nil } + +func (a *Add) resourcePrint(fileName string, res resource.ResourceRead) { + resMap := map[string]resource.ResourceRead{res.ID(): res} + + oj, _ := marshal(resMap) + typ := reflect.TypeOf(res) + typs := strings.Split(typ.String(), ".")[1] + + fmt.Fprintf(a.Writer, "Adding %s to '%s':\n\n%s\n\n", typs, fileName, string(oj)) +} diff --git a/add_test.go b/add_test.go index a479cea..83f4089 100644 --- a/add_test.go +++ b/add_test.go @@ -1,8 +1,8 @@ package goss import ( - "github.com/stretchr/testify/assert" - "testing" + "github.com/stretchr/testify/assert" + "testing" ) func Test_ExtractHeaderArgument(t *testing.T) { diff --git a/app.go b/app.go new file mode 100644 index 0000000..0ef1efa --- /dev/null +++ b/app.go @@ -0,0 +1,92 @@ +package goss + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +// GossRunTime represents the global runtime configs which can be set in goss +type GossRunTime struct { + //Gossfile which should holds the test config + Gossfile string + //Vars file which holds the variabesl + Vars string + //Package defines which package manager you want to use, i.e. yum, apt, ... + Package string //this does not belong here imho + //Debug on true will create a more verbose output + Debug bool +} + +// Serve serves a new health endpoint +func (g *GossRunTime) Serve(endpoint string, handler *HealthHandler) { + handler.Serve(endpoint) +} + +// Validate starts the validation process +func (g *GossRunTime) Validate(v *Validator) int { + return v.Validate(time.Now()) +} + +// Render renders a template file +func (g *GossRunTime) Render() (string, error) { + goss, err := os.Open(g.Gossfile) + if err != nil { + return "", fmt.Errorf("Could not open gossfile with error: %s", err.Error()) + } + defer goss.Close() + + vars, err := os.Open(g.Vars) + if err != nil { + return "", fmt.Errorf("Could not open varsfile with error: %s", err.Error()) + } + defer vars.Close() + + return RenderJSON(goss, vars), nil +} + +// GetGossConfig returns the goss configuration +func (g *GossRunTime) GetGossConfig() GossConfig { + // handle stdin + var fh *os.File + var path, source string + var gossConfig GossConfig + TemplateFilter = NewTemplateFilter(g.Vars) + specFile := g.Gossfile + if specFile == "-" { + source = "STDIN" + fh = os.Stdin + data, err := ioutil.ReadAll(fh) + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + OutStoreFormat = getStoreFormatFromData(data) + gossConfig = ReadJSONData(data, true) + } else if specFile == "testing" { + json := []byte(` +command: + echo hello: + exit-status: 0 + stdout: + - hello + timeout: 10000 +`) + gossConfig = ReadJSONData(json, true) + } else { + source = specFile + path = filepath.Dir(specFile) + OutStoreFormat = getStoreFormatFromFileName(specFile) + gossConfig = ReadJSON(specFile) + } + + gossConfig = mergeJSONData(gossConfig, 0, path) + + if len(gossConfig.Resources()) == 0 { + fmt.Printf("Error: found 0 tests, source: %v\n", source) + os.Exit(1) + } + return gossConfig +} diff --git a/app_test.go b/app_test.go new file mode 100644 index 0000000..aada672 --- /dev/null +++ b/app_test.go @@ -0,0 +1,32 @@ +package goss + +import ( + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "testing" +) + +func Test_Render_WithoutGossfile(t *testing.T) { + runtime := GossRunTime{} + result, err := runtime.Render() + + assert.NotNil(t, err) + assert.Equal(t, "Could not open gossfile with error: open : no such file or directory", err.Error()) + assert.Empty(t, result) +} + +func Test_Render_WithoutVarsfile(t *testing.T) { + file, err := ioutil.TempFile("", "tmp_gossfile_*.yaml") + defer os.Remove(file.Name()) + + runtime := GossRunTime{ + Gossfile: file.Name(), + Vars: "/invalidpath", + } + result, err := runtime.Render() + + assert.NotNil(t, err) + assert.Equal(t, "Could not open varsfile with error: open /invalidpath: no such file or directory", err.Error()) + assert.Empty(t, result) +} diff --git a/cmd/goss/goss.go b/cmd/goss/goss.go index 12cf59a..8fa8de5 100644 --- a/cmd/goss/goss.go +++ b/cmd/goss/goss.go @@ -2,21 +2,83 @@ package main import ( "fmt" + "github.com/SimonBaeumer/goss/system" + "github.com/fatih/color" + "github.com/patrickmn/go-cache" "log" "os" + "sync" "time" "github.com/SimonBaeumer/goss" "github.com/SimonBaeumer/goss/outputs" "github.com/urfave/cli" - //"time" ) var version string func main() { + app := createApp() - startTime := time.Now() + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +// CliConfig represents all configurations passed by the cli app +type CliConfig struct { + FormatOptions []string + Sleep time.Duration + RetryTimeout time.Duration + MaxConcurrent int + Vars string + Gossfile string + ExcludeAttr []string + Timeout int + AllowInsecure bool + NoFollowRedirects bool + Server string + Username string + Password string + Header string + Endpoint string + ListenAddr string + Cache time.Duration + Format string + NoColor bool + Color bool +} + +// NewCliConfig creates an object from the cli.Context. It is used as a constructor and will do simple type conversions +// and validations +func NewCliConfig(c *cli.Context) CliConfig { + return CliConfig{ + FormatOptions: c.StringSlice("format-options"), + Sleep: c.Duration("sleep"), + RetryTimeout: c.Duration("retry-timeout"), + MaxConcurrent: c.Int("max-concurrent"), + Vars: c.GlobalString("vars"), + Gossfile: c.GlobalString("gossfile"), + ExcludeAttr: c.GlobalStringSlice("exclude-attr"), + Timeout: int(c.Duration("timeout") / time.Millisecond), + AllowInsecure: c.Bool("insecure"), + NoFollowRedirects: c.Bool("no-follow-redirects"), + Server: c.String("server"), + Username: c.String("username"), + Password: c.String("password"), + Header: c.String("header"), + Endpoint: c.String("endpoint"), + ListenAddr: c.String("listen-addr"), + Cache: c.Duration("cache"), + Format: c.String("format"), + NoColor: c.Bool("no-color"), + Color: c.Bool("color"), + } +} + +// Create the cli.App object +func createApp() *cli.App { app := cli.NewApp() app.EnableBashCompletion = true app.Version = version @@ -26,7 +88,7 @@ func main() { cli.StringFlag{ Name: "gossfile, g", Value: "./goss.yaml", - Usage: "Goss file to read from / write to", + Usage: "GossRunTime file to read from / write to", EnvVar: "GOSS_FILE", }, cli.StringFlag{ @@ -34,108 +96,11 @@ func main() { Usage: "json/yaml file containing variables for template", EnvVar: "GOSS_VARS", }, - cli.StringFlag{ - Name: "package", - Usage: "Package type to use [rpm, deb, apk, pacman]", - }, } app.Commands = []cli.Command{ - { - Name: "validate", - Aliases: []string{"v"}, - Usage: "Validate system", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "format, f", - Value: "rspecish", - Usage: fmt.Sprintf("Format to output in, valid options: %s", outputs.Outputers()), - EnvVar: "GOSS_FMT", - }, - cli.StringSliceFlag{ - Name: "format-options, o", - Usage: fmt.Sprintf("Extra options passed to the formatter, valid options: %s", outputs.FormatOptions()), - EnvVar: "GOSS_FMT_OPTIONS", - }, - cli.BoolFlag{ - Name: "color", - Usage: "Force color on", - EnvVar: "GOSS_COLOR", - }, - cli.BoolFlag{ - Name: "no-color", - Usage: "Force color off", - EnvVar: "GOSS_NOCOLOR", - }, - cli.DurationFlag{ - Name: "sleep,s", - Usage: "Time to sleep between retries, only active when -r is set", - Value: 1 * time.Second, - EnvVar: "GOSS_SLEEP", - }, - cli.DurationFlag{ - Name: "retry-timeout,r", - Usage: "Retry on failure so long as elapsed + sleep time is less than this", - Value: 0, - EnvVar: "GOSS_RETRY_TIMEOUT", - }, - cli.IntFlag{ - Name: "max-concurrent", - Usage: "Max number of tests to run concurrently", - Value: 50, - EnvVar: "GOSS_MAX_CONCURRENT", - }, - }, - Action: func(c *cli.Context) error { - goss.Validate(c, startTime) - return nil - }, - }, - { - Name: "serve", - Aliases: []string{"s"}, - Usage: "Serve a health endpoint", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "format, f", - Value: "rspecish", - Usage: fmt.Sprintf("Format to output in, valid options: %s", outputs.Outputers()), - EnvVar: "GOSS_FMT", - }, - cli.StringSliceFlag{ - Name: "format-options, o", - Usage: fmt.Sprintf("Extra options passed to the formatter, valid options: %s", outputs.FormatOptions()), - EnvVar: "GOSS_FMT_OPTIONS", - }, - cli.DurationFlag{ - Name: "cache,c", - Usage: "Time to cache the results", - Value: 5 * time.Second, - EnvVar: "GOSS_CACHE", - }, - cli.StringFlag{ - Name: "listen-addr,l", - Value: ":8080", - Usage: "Address to listen on [ip]:port", - EnvVar: "GOSS_LISTEN", - }, - cli.StringFlag{ - Name: "endpoint,e", - Value: "/healthz", - Usage: "Endpoint to expose", - EnvVar: "GOSS_ENDPOINT", - }, - cli.IntFlag{ - Name: "max-concurrent", - Usage: "Max number of tests to run concurrently", - Value: 50, - EnvVar: "GOSS_MAX_CONCURRENT", - }, - }, - Action: func(c *cli.Context) error { - goss.Serve(c) - return nil - }, - }, + createValidateCommand(app), + createServeCommand(), + createAddCommand(app), { Name: "render", Aliases: []string{"r"}, @@ -147,7 +112,13 @@ func main() { }, }, Action: func(c *cli.Context) error { - fmt.Print(goss.RenderJSON(c)) + conf := NewCliConfig(c) + runtime := goss.GossRunTime{ + Vars: conf.Vars, + Gossfile: conf.Gossfile, + } + + fmt.Print(runtime.Render()) return nil }, }, @@ -156,193 +127,340 @@ func main() { Aliases: []string{"aa"}, Usage: "automatically add all matching resource to the test suite", Action: func(c *cli.Context) error { - goss.AutoAddResources(c.GlobalString("gossfile"), c.Args(), c) - return nil + conf := NewCliConfig(c) + + a := goss.Add{ + Writer: app.Writer, + Sys: system.New(), + Username: conf.Username, + Password: conf.Password, + Server: conf.Server, + Timeout: conf.Timeout, + NoFollowRedirects: conf.NoFollowRedirects, + AllowInsecure: conf.AllowInsecure, + ExcludeAttr: conf.ExcludeAttr, + Header: conf.Header, + } + return a.AutoAddResources(c.GlobalString("gossfile"), c.Args()) }, }, - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a resource to the test suite", - Flags: []cli.Flag{ - cli.StringSliceFlag{ - Name: "exclude-attr", - Usage: "Exclude the following attributes when adding a new resource", - }, + } + return app +} + +func createServeCommand() cli.Command { + return cli.Command{ + Name: "serve", + Aliases: []string{"s"}, + Usage: "Serve a health endpoint", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "format, f", + Value: "rspecish", + Usage: fmt.Sprintf("Format to output in, valid options: %s", outputs.Outputers()), + EnvVar: "GOSS_FMT", }, - Subcommands: []cli.Command{ - { - Name: "package", - Usage: "add new package", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Package", c.Args(), c) - return nil - }, - }, - { - Name: "file", - Usage: "add new file", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "File", c.Args(), c) - return nil - }, - }, - { - Name: "addr", - Usage: "add new remote address:port - ex: google.com:80", - Flags: []cli.Flag{ - cli.DurationFlag{ - Name: "timeout", - Value: 500 * time.Millisecond, - }, - }, - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Addr", c.Args(), c) - return nil - }, - }, - { - Name: "port", - Usage: "add new listening [protocol]:port - ex: 80 or udp:123", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Port", c.Args(), c) - return nil - }, - }, - { - Name: "service", - Usage: "add new service", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Service", c.Args(), c) - return nil - }, - }, - { - Name: "user", - Usage: "add new user", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "User", c.Args(), c) - return nil - }, - }, - { - Name: "group", - Usage: "add new group", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Group", c.Args(), c) - return nil + cli.StringSliceFlag{ + Name: "format-options, o", + Usage: fmt.Sprintf("Extra options passed to the formatter, valid options: %s", outputs.FormatOptions()), + EnvVar: "GOSS_FMT_OPTIONS", + }, + cli.DurationFlag{ + Name: "cache,c", + Usage: "Time to cache the results", + Value: 5 * time.Second, + EnvVar: "GOSS_CACHE", + }, + cli.StringFlag{ + Name: "listen-addr,l", + Value: ":8080", + Usage: "Address to listen on [ip]:port", + EnvVar: "GOSS_LISTEN", + }, + cli.StringFlag{ + Name: "endpoint,e", + Value: "/healthz", + Usage: "Endpoint to expose", + EnvVar: "GOSS_ENDPOINT", + }, + cli.IntFlag{ + Name: "max-concurrent", + Usage: "Max number of tests to run concurrently", + Value: 50, + EnvVar: "GOSS_MAX_CONCURRENT", + }, + }, + Action: func(c *cli.Context) error { + conf := NewCliConfig(c) + + gossRunTime := goss.GossRunTime{ + Gossfile: c.GlobalString("gossfile"), + Vars: c.GlobalString("vars"), + } + + h := &goss.HealthHandler{ + Cache: cache.New(conf.Cache, 30*time.Second), + Outputer: outputs.GetOutputer(conf.Format), + Sys: system.New(), + GossMu: &sync.Mutex{}, + MaxConcurrent: conf.MaxConcurrent, + GossConfig: gossRunTime.GetGossConfig(), + FormatOptions: conf.FormatOptions, + } + + if conf.Format == "json" { + h.ContentType = "application/json" + } + + gossRunTime.Serve(conf.Endpoint, h) + return nil + }, + } +} + +func createAddHandler(app *cli.App, resourceName string) func(c *cli.Context) error { + return func(c *cli.Context) error { + conf := NewCliConfig(c) + + a := goss.Add{ + Sys: system.New(), + Writer: app.Writer, + Username: conf.Username, + Password: conf.Password, + Server: conf.Server, + Timeout: conf.Timeout, + NoFollowRedirects: conf.NoFollowRedirects, + AllowInsecure: conf.AllowInsecure, + ExcludeAttr: conf.ExcludeAttr, + Header: conf.Header, + } + + a.AddResources(c.GlobalString("gossfile"), resourceName, c.Args()) + return nil + } +} + +func createAddCommand(app *cli.App) cli.Command { + return cli.Command{ + Name: "add", + Aliases: []string{"a"}, + Usage: "add a resource to the test suite", + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "exclude-attr", + Usage: "Exclude the following attributes when adding a new resource", + }, + }, + Subcommands: []cli.Command{ + { + Name: "Package", + Usage: "add new package", + Action: createAddHandler(app, "package"), + }, + { + Name: "file", + Usage: "add new file", + Action: createAddHandler(app, "file"), + }, + { + Name: "addr", + Usage: "add new remote address:port - ex: google.com:80", + Flags: []cli.Flag{ + cli.DurationFlag{ + Name: "timeout", + Value: 500 * time.Millisecond, }, }, - { - Name: "command", - Usage: "add new command", - Flags: []cli.Flag{ - cli.DurationFlag{ - Name: "timeout", - Value: 10 * time.Second, - }, - }, - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Command", c.Args(), c) - return nil + Action: createAddHandler(app, "addr"), + }, + { + Name: "port", + Usage: "add new listening [protocol]:port - ex: 80 or udp:123", + Action: createAddHandler(app, "port"), + }, + { + Name: "service", + Usage: "add new service", + Action: createAddHandler(app, "service"), + }, + { + Name: "user", + Usage: "add new user", + Action: createAddHandler(app, "user"), + }, + { + Name: "group", + Usage: "add new group", + Action: createAddHandler(app, "group"), + }, + { + Name: "command", + Usage: "add new command", + Flags: []cli.Flag{ + cli.DurationFlag{ + Name: "timeout", + Value: 10 * time.Second, }, }, - { - Name: "dns", - Usage: "add new dns", - Flags: []cli.Flag{ - cli.DurationFlag{ - Name: "timeout", - Value: 500 * time.Millisecond, - }, - cli.StringFlag{ - Name: "server", - Usage: "The IP address of a DNS server to query", - }, + Action: createAddHandler(app, "command"), + }, + { + Name: "dns", + Usage: "add new dns", + Flags: []cli.Flag{ + cli.DurationFlag{ + Name: "timeout", + Value: 500 * time.Millisecond, }, - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "DNS", c.Args(), c) - return nil + cli.StringFlag{ + Name: "server", + Usage: "The IP address of a DNS server to query", }, }, - { - Name: "process", - Usage: "add new process name", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Process", c.Args(), c) - return nil + Action: createAddHandler(app, "dns"), + }, + { + Name: "process", + Usage: "add new process name", + Action: createAddHandler(app, "process"), + }, + { + Name: "http", + Usage: "add new http", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "insecure, k", }, - }, - { - Name: "http", - Usage: "add new http", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "insecure, k", - }, - cli.BoolFlag{ - Name: "no-follow-redirects, r", - }, - cli.DurationFlag{ - Name: "timeout", - Value: 5 * time.Second, - }, - cli.StringFlag{ - Name: "username, u", - Usage: "Username for basic auth", - }, - cli.StringFlag{ - Name: "password, p", - Usage: "Password for basic auth", - }, - cli.StringFlag{ - Name: "header", - Usage: "Set-Cookie: Value", - }, + cli.BoolFlag{ + Name: "no-follow-redirects, r", }, - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "HTTP", c.Args(), c) - return nil + cli.DurationFlag{ + Name: "timeout", + Value: 5 * time.Second, }, - }, - { - Name: "goss", - Usage: "add new goss file, it will be imported from this one", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Gossfile", c.Args(), c) - return nil + cli.StringFlag{ + Name: "username, u", + Usage: "Username for basic auth", }, - }, - { - Name: "kernel-param", - Usage: "add new goss kernel param", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "KernelParam", c.Args(), c) - return nil - }, - }, - { - Name: "mount", - Usage: "add new mount", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Mount", c.Args(), c) - return nil + cli.StringFlag{ + Name: "password, p", + Usage: "Password for basic auth", }, - }, - { - Name: "interface", - Usage: "add new interface", - Action: func(c *cli.Context) error { - goss.AddResources(c.GlobalString("gossfile"), "Interface", c.Args(), c) - return nil + cli.StringFlag{ + Name: "header", + Usage: "Set-Cookie: Value", }, }, + Action: createAddHandler(app, "http"), + }, + { + Name: "goss", + Usage: "add new goss file, it will be imported from this one", + Action: createAddHandler(app, "goss"), + }, + { + Name: "kernel-param", + Usage: "add new goss kernel param", + Action: createAddHandler(app, "kernel-param"), + }, + { + Name: "mount", + Usage: "add new mount", + Action: createAddHandler(app, "mount"), + }, + { + Name: "interface", + Usage: "add new interface", + Action: createAddHandler(app, "interface"), }, }, } +} - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) +func createValidateCommand(app *cli.App) cli.Command { + startTime := time.Now() + + return cli.Command{ + Name: "validate", + Aliases: []string{"v"}, + Usage: "Validate system", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "format, f", + Value: "rspecish", + Usage: fmt.Sprintf("Format to output in, valid options: %s", outputs.Outputers()), + EnvVar: "GOSS_FMT", + }, + cli.StringSliceFlag{ + Name: "format-options, o", + Usage: fmt.Sprintf("Extra options passed to the formatter, valid options: %s", outputs.FormatOptions()), + EnvVar: "GOSS_FMT_OPTIONS", + }, + cli.BoolFlag{ + Name: "color", + Usage: "Force color on", + EnvVar: "GOSS_COLOR", + }, + cli.BoolFlag{ + Name: "no-color", + Usage: "Force color off", + EnvVar: "GOSS_NOCOLOR", + }, + cli.DurationFlag{ + Name: "sleep,s", + Usage: "Time to sleep between retries, only active when -r is set", + Value: 1 * time.Second, + EnvVar: "GOSS_SLEEP", + }, + cli.DurationFlag{ + Name: "retry-timeout,r", + Usage: "Retry on failure so long as elapsed + sleep time is less than this", + Value: 0, + EnvVar: "GOSS_RETRY_TIMEOUT", + }, + cli.IntFlag{ + Name: "max-concurrent", + Usage: "Max number of tests to run concurrently", + Value: 50, + EnvVar: "GOSS_MAX_CONCURRENT", + }, + }, + Action: func(c *cli.Context) error { + conf := NewCliConfig(c) + + runtime := getGossRunTime(conf.Gossfile, conf.Vars) + + v := &goss.Validator{ + OutputWriter: app.Writer, + MaxConcurrent: conf.MaxConcurrent, + Outputer: outputs.GetOutputer(conf.Format), + FormatOptions: conf.FormatOptions, + GossConfig: runtime.GetGossConfig(), + } + + //TODO: ugly shit to set the color here, tmp fix for the moment! + if conf.NoColor { + color.NoColor = true + } + if conf.Color { + color.NoColor = false + } + + if conf.Gossfile == "testing" { + v.Validate(startTime) + return nil + } + + os.Exit(runtime.Validate(v)) + return nil + }, + } +} + +func getGossRunTime(gossfile string, vars string) goss.GossRunTime { + runtime := goss.GossRunTime{ + Gossfile: gossfile, + Vars: vars, } + return runtime } diff --git a/cmd/goss/goss_test.go b/cmd/goss/goss_test.go new file mode 100644 index 0000000..2ef63ef --- /dev/null +++ b/cmd/goss/goss_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "io/ioutil" + "testing" +) + +func TestApp_Validate(t *testing.T) { + b := &bytes.Buffer{} + app := createApp() + app.Writer = b + + r := app.Run([]string{"", "--gossfile", "testing", "validate"}) + + assert.Nil(t, r) + assert.Contains(t, b.String(), "Count: 2, Failed: 0, Skipped: 0") +} + +func TestApp_Add(t *testing.T) { + b := &bytes.Buffer{} + app := createApp() + app.Writer = b + + file, err := ioutil.TempFile("/tmp", "testing_goss_*.yaml") + if err != nil { + panic(err.Error()) + } + defer file.Close() + + r := app.Run([]string{"", "--gossfile", file.Name(), "add", "http", "http://google.com"}) + + assert.Nil(t, r) + assert.Contains(t, b.String(), getAddResult()) + assert.Contains(t, b.String(), "Adding HTTP to '/tmp/testing_goss_") +} + +func getAddResult() string { + return `http://google.com: + status: 200 + allow-insecure: false + no-follow-redirects: false + timeout: 5000 + body: []` +} diff --git a/development/http/http_test_server.go b/development/http/http_test_server.go index c8accb2..0c568da 100644 --- a/development/http/http_test_server.go +++ b/development/http/http_test_server.go @@ -39,4 +39,4 @@ func redirectHandler(w http.ResponseWriter, r *http.Request) { log.Println("/redirect") printHeader(r) http.Redirect(w, r, "/", RedirectStatusCode) -} \ No newline at end of file +} diff --git a/development/ssl/https_server.go b/development/ssl/https_server.go index 02fe619..afaba26 100644 --- a/development/ssl/https_server.go +++ b/development/ssl/https_server.go @@ -1,47 +1,47 @@ package main import ( - "crypto/tls" - "crypto/x509" - "io/ioutil" - "log" - "net/http" + "crypto/tls" + "crypto/x509" + "io/ioutil" + "log" + "net/http" ) // Create a minimal https server with // client cert authentication func main() { - // Add root ca - caCert, _ := ioutil.ReadFile("ca.crt") - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) + // Add root ca + caCert, _ := ioutil.ReadFile("ca.crt") + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) - // Add valid client certificates - clientCert, _ := ioutil.ReadFile("client.crt") - clientCAs := x509.NewCertPool() - clientCAs.AppendCertsFromPEM(clientCert) + // Add valid client certificates + clientCert, _ := ioutil.ReadFile("client.crt") + clientCAs := x509.NewCertPool() + clientCAs.AppendCertsFromPEM(clientCert) - server := &http.Server{ - Addr: ":8081", - TLSConfig: &tls.Config{ - ClientAuth: tls.RequireAndVerifyClientCert, - RootCAs: caCertPool, - ClientCAs: clientCAs, - }, - } - server.Handler = &handler{} + server := &http.Server{ + Addr: ":8081", + TLSConfig: &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + RootCAs: caCertPool, + ClientCAs: clientCAs, + }, + } + server.Handler = &handler{} - err := server.ListenAndServeTLS("server.crt", "server.key") - if err != nil { - log.Fatal(err.Error()) - } + err := server.ListenAndServeTLS("server.crt", "server.key") + if err != nil { + log.Fatal(err.Error()) + } } type handler struct{} // ServeHTTP serves the handler function func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if _, err := w.Write([]byte("PONG\n")); err != nil { - log.Fatal(err.Error()) - } -} \ No newline at end of file + if _, err := w.Write([]byte("PONG\n")); err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/manual.md b/docs/manual.md index 705b624..a54a4d3 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -702,12 +702,11 @@ package: # required attributes installed: true # optional attributes + package: pacman #set a specific package manager versions: - 2.2.15 ``` -**NOTE:** this check uses the `--package ` parameter passed on the command line. - ### port Validates the state of a local port. diff --git a/go.mod b/go.mod index fc6a596..c17a0de 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,11 @@ require ( github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 github.com/aelsabbahy/GOnetstat v0.0.0-20160428114218-edf89f784e08 github.com/aelsabbahy/go-ps v0.0.0-20170721000941-443386855ca1 + github.com/alecthomas/chroma v0.6.3 github.com/cheekybits/genny v0.0.0-20160824153601-e8e29e67948b - github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/docker v0.0.0-20161109014415-383a2f046b16 github.com/fatih/color v0.0.0-20161025120501-bf82308e8c85 github.com/golang/mock v1.2.0 - github.com/mattn/go-colorable v0.0.9 // indirect - github.com/mattn/go-isatty v0.0.4 // indirect github.com/miekg/dns v0.0.0-20161018060808-58f52c57ce9d github.com/oleiade/reflections v0.0.0-20160817071559-0e86b3c98b2f github.com/onsi/gomega v1.5.0 diff --git a/go.sum b/go.sum index 7b8ef2c..3887fe8 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,21 @@ github.com/aelsabbahy/GOnetstat v0.0.0-20160428114218-edf89f784e08 h1:oD15ssIOuF github.com/aelsabbahy/GOnetstat v0.0.0-20160428114218-edf89f784e08/go.mod h1:FETZSu2VGNDJbGfeRExaz/SNbX0TTaqJEMo1yvsKoZ8= github.com/aelsabbahy/go-ps v0.0.0-20170721000941-443386855ca1 h1:s4dvLggvQOov0YFdv8XQvX+72TAFzfJg+6SgoXiIaq4= github.com/aelsabbahy/go-ps v0.0.0-20170721000941-443386855ca1/go.mod h1:70tSBushy/POz6cCR294bKno4BNAC7XWVdkkxWQ1N6E= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.6.3 h1:8H1D0yddf0mvgvO4JDBKnzLd9ERmzzAijBxnZXGV/FA= +github.com/alecthomas/chroma v0.6.3/go.mod h1:quT2EpvJNqkuPi6DmBHB+E33FXBgBBPzyH5++Dn1LPc= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.1.15/go.mod h1:0m2VYms8rH0qbCqVB2gvGHk74bqLIq0HXjCs5bNbNQU= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/cheekybits/genny v0.0.0-20160824153601-e8e29e67948b h1:EaV7ZKUbpQK3eErRkV5GKl7s6SZU30dEB6gimH5BLLk= github.com/cheekybits/genny v0.0.0-20160824153601-e8e29e67948b/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/docker v0.0.0-20161109014415-383a2f046b16 h1:8J7CV9qtX4ygmYnM9uL5Ola9iI9QWzDt0rI3rwKSfSo= github.com/docker/docker v0.0.0-20161109014415-383a2f046b16/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/fatih/color v0.0.0-20161025120501-bf82308e8c85 h1:g7ijd5QIEMWwZNVp/T/6kQ8RSh8rN+YNhghMcrET3qY= @@ -37,7 +47,9 @@ github.com/patrickmn/go-cache v2.0.0+incompatible h1:1G02Ver4lZNbrWBHtot9O0Z2Pik github.com/patrickmn/go-cache v2.0.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/urfave/cli v0.0.0-20161102131801-d86a009f5e13 h1:niRuEF0NOlFnqraxzjuvvOdCM6gxmHiaBABjvg3/kDo= @@ -46,6 +58,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181210030007-2a47403f2ae5 h1:SlFRMb9PEnqzqnBRCynVOhxv4vHjB2lnIoxK6p5nzFM= golang.org/x/sys v0.0.0-20181210030007-2a47403f2ae5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= diff --git a/integration/TODO.md b/integration/TODO.md index 1b96ed7..c429998 100644 --- a/integration/TODO.md +++ b/integration/TODO.md @@ -5,7 +5,7 @@ Fail and Success case for every resource. - [x] addr - [x] command - - [ ] dns + - [x] dns - [x] file - [x] gossfile - [x] group diff --git a/integration/commander.yaml b/integration/commander.yaml index 7ebc96a..ba8bebc 100755 --- a/integration/commander.yaml +++ b/integration/commander.yaml @@ -400,7 +400,7 @@ tests: - |- Package: apt: version: Expected - <[]string | len:1, cap:1>: ["1.6.8"] + <[]string | len:1, cap:1>: ["1.6.11"] to contain element matching : 100.0.0 - |- diff --git a/integration/resources/package/ubuntu/goss.yaml b/integration/resources/package/ubuntu/goss.yaml index 73d4750..8026b13 100644 --- a/integration/resources/package/ubuntu/goss.yaml +++ b/integration/resources/package/ubuntu/goss.yaml @@ -2,6 +2,6 @@ package: apt: installed: true versions: - - 1.6.8 + - 1.6.11 no exists: installed: false \ No newline at end of file diff --git a/novendor.sh b/novendor.sh index 2b57337..dad96c8 100755 --- a/novendor.sh +++ b/novendor.sh @@ -9,6 +9,11 @@ DIRS=$(ls -ld */ . | awk {'print $9'} | grep -v vendor) for DIR in ${DIRS}; do GOFILES=$(git ls-files ${DIR} | grep ".*\.go$") || true + # ignore dev directory... + if [[ ${DIR} == "development/" ]]; then + continue + fi + if [[ ${DIR} == "." ]]; then echo "." continue diff --git a/outputs/documentation.go b/outputs/documentation.go index f4d65b7..cc5af90 100644 --- a/outputs/documentation.go +++ b/outputs/documentation.go @@ -9,7 +9,14 @@ import ( "github.com/SimonBaeumer/goss/util" ) -type Documentation struct{} +// Documentation represents the documentation output type +type Documentation struct { + //FakeDuration will only be used for testing purposes + FakeDuration time.Duration +} + +// Name returns the name +func (r Documentation) Name() string { return "documentation" } func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { @@ -47,7 +54,11 @@ func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult, fmt.Fprint(w, "\n\n") fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped)) - fmt.Fprint(w, summary(startTime, testCount, failed, skipped)) + duration := time.Since(startTime) + if r.FakeDuration != 0 { + duration = r.FakeDuration + } + fmt.Fprint(w, summary(duration.Seconds(), testCount, failed, skipped)) if failed > 0 { return 1 } diff --git a/outputs/documentation_test.go b/outputs/documentation_test.go new file mode 100644 index 0000000..c5998c7 --- /dev/null +++ b/outputs/documentation_test.go @@ -0,0 +1,45 @@ +package outputs + +import ( + "bytes" + "github.com/SimonBaeumer/goss/resource" + "github.com/SimonBaeumer/goss/util" + "github.com/stretchr/testify/assert" + "sync" + "testing" + "time" +) + +func TestDocumentation_Name(t *testing.T) { + j := Documentation{} + assert.Equal(t, "documentation", j.Name()) +} + +func TestDocumentation_Output(t *testing.T) { + var wg sync.WaitGroup + b := &bytes.Buffer{} + d, _ := time.ParseDuration("2s") + j := Documentation{FakeDuration: d} + out := make(chan []resource.TestResult) + r := 1 + + go func() { + defer wg.Done() + wg.Add(1) + r = j.Output(b, out, time.Now(), util.OutputConfig{}) + }() + + out <- GetExampleTestResult() + + close(out) + wg.Wait() + expectedJson := `Title: my title +resource type: my resource id: a property: matches expectation: [expected] + + +Total Duration: 2.000s +Count: 1, Failed: 0, Skipped: 0 +` + assert.Equal(t, expectedJson, b.String()) + assert.Equal(t, 0, r) +} diff --git a/outputs/json.go b/outputs/json.go index babcf8f..c2611bf 100644 --- a/outputs/json.go +++ b/outputs/json.go @@ -11,8 +11,16 @@ import ( "github.com/fatih/color" ) -type Json struct{} +// Json represents the json output type +type Json struct { + // FakeDuration will only be used for testing purposes + FakeDuration time.Duration +} + +// Name returns the name +func (r Json) Name() string { return "json" } +// Output writes the actual output func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { @@ -35,6 +43,10 @@ func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, summary := make(map[string]interface{}) duration := time.Since(startTime) + if r.FakeDuration != 0 { + duration = r.FakeDuration + } + summary["test-count"] = testCount summary["failed-count"] = failed summary["total-duration"] = duration diff --git a/outputs/json_oneline.go b/outputs/json_oneline.go index 8453e8d..65a355f 100644 --- a/outputs/json_oneline.go +++ b/outputs/json_oneline.go @@ -11,8 +11,13 @@ import ( "github.com/fatih/color" ) +// JsonOneline represents the JsonOneline output type type JsonOneline struct{} +// Name returns the name +func (r JsonOneline) Name() string { return "json_oneline" } + +// Output writes the actual output func (r JsonOneline) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { diff --git a/outputs/json_test.go b/outputs/json_test.go new file mode 100644 index 0000000..b571dd2 --- /dev/null +++ b/outputs/json_test.go @@ -0,0 +1,66 @@ +package outputs + +import ( + "bytes" + "github.com/SimonBaeumer/goss/resource" + "github.com/SimonBaeumer/goss/util" + "github.com/stretchr/testify/assert" + "sync" + "testing" + "time" +) + +func TestJson_Name(t *testing.T) { + j := Json{} + assert.Equal(t, "json", j.Name()) +} + +func TestJson_Output(t *testing.T) { + var wg sync.WaitGroup + b := &bytes.Buffer{} + j := Json{FakeDuration: 1000} + out := make(chan []resource.TestResult) + r := 1 + + go func() { + defer wg.Done() + wg.Add(1) + r = j.Output(b, out, time.Now(), util.OutputConfig{}) + }() + + out <- GetExampleTestResult() + + close(out) + wg.Wait() + expectedJson := `{ + "results": [ + { + "duration": 500, + "err": null, + "expected": [ + "expected" + ], + "found": null, + "human": "", + "meta": null, + "property": "a property", + "resource-id": "my resource id", + "resource-type": "resource type", + "result": 0, + "successful": true, + "summary-line": "resource type: my resource id: a property: matches expectation: [expected]", + "test-type": 0, + "title": "my title" + } + ], + "summary": { + "failed-count": 0, + "summary-line": "Count: 1, Failed: 0, Duration: 0.000s", + "test-count": 1, + "total-duration": 1000 + } +} +` + assert.Equal(t, expectedJson, b.String()) + assert.Equal(t, 0, r) +} diff --git a/outputs/junit.go b/outputs/junit.go index 480a90b..12c5b2b 100644 --- a/outputs/junit.go +++ b/outputs/junit.go @@ -13,8 +13,13 @@ import ( "github.com/fatih/color" ) +// JUnit represents the junit output type type JUnit struct{} +// Name returns the name +func (r JUnit) Name() string { return "junit" } + +// Output writes the actual output func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { diff --git a/outputs/nagios.go b/outputs/nagios.go index d7a080e..eee3722 100644 --- a/outputs/nagios.go +++ b/outputs/nagios.go @@ -10,8 +10,13 @@ import ( "github.com/SimonBaeumer/goss/util" ) +// Nagios represents the nagios output type type Nagios struct{} +// Name returns the name +func (r Nagios) Name() string { return "nagios" } + +// Output writes the actual output func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { diff --git a/outputs/outputs.go b/outputs/outputs.go index 22f1c22..994e11d 100644 --- a/outputs/outputs.go +++ b/outputs/outputs.go @@ -14,8 +14,10 @@ import ( "github.com/fatih/color" ) +// Outputer is the interface which is used for the generation of the view type Outputer interface { Output(io.Writer, <-chan []resource.TestResult, time.Time, util.OutputConfig) int + Name() string } var green = color.New(color.FgGreen).SprintfFunc() @@ -84,6 +86,7 @@ var ( outputerFormatOptions = make(map[string][]string) ) +// RegisterOutputer registers a new outputer in the registry func RegisterOutputer(name string, outputer Outputer, formatOptions []string) { outputersMu.Lock() defer outputersMu.Unlock() @@ -125,6 +128,7 @@ func FormatOptions() []string { return list } +//GetOutputer returns an outputer by name func GetOutputer(name string) Outputer { if _, ok := outputers[name]; !ok { fmt.Println("goss: Bad output format: " + name) @@ -171,9 +175,9 @@ func header(t resource.TestResult) string { return out } -func summary(startTime time.Time, count, failed, skipped int) string { +func summary(duration float64, count, failed, skipped int) string { var s string - s += fmt.Sprintf("Total Duration: %.3fs\n", time.Since(startTime).Seconds()) + s += fmt.Sprintf("Total Duration: %.3fs\n", duration) f := green if failed > 0 { f = red @@ -181,6 +185,7 @@ func summary(startTime time.Time, count, failed, skipped int) string { s += f("Count: %d, Failed: %d, Skipped: %d\n", count, failed, skipped) return s } + func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult) string { var s string if len(failedOrSkipped) > 0 { diff --git a/outputs/rspecish.go b/outputs/rspecish.go index 2d72026..209f4c4 100644 --- a/outputs/rspecish.go +++ b/outputs/rspecish.go @@ -9,8 +9,16 @@ import ( "github.com/SimonBaeumer/goss/util" ) -type Rspecish struct{} +// Rspecish represents the rspecish output type +type Rspecish struct { + //FakeDuration will only be needed for testing purposes + FakeDuration time.Duration +} + +// Name returns the name +func (r Rspecish) Name() string { return "rspecish" } +// Output writes the actual output func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { @@ -42,7 +50,11 @@ func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult, fmt.Fprint(w, "\n\n") fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped)) - fmt.Fprint(w, summary(startTime, testCount, failed, skipped)) + duration := time.Since(startTime) + if r.FakeDuration != 0 { + duration = r.FakeDuration + } + fmt.Fprint(w, summary(duration.Seconds(), testCount, failed, skipped)) if failed > 0 { return 1 } diff --git a/outputs/rspecish_test.go b/outputs/rspecish_test.go new file mode 100644 index 0000000..7c3e6b2 --- /dev/null +++ b/outputs/rspecish_test.go @@ -0,0 +1,43 @@ +package outputs + +import ( + "bytes" + "github.com/SimonBaeumer/goss/resource" + "github.com/SimonBaeumer/goss/util" + "github.com/stretchr/testify/assert" + "sync" + "testing" + "time" +) + +func TestRspecish_Name(t *testing.T) { + j := Rspecish{} + assert.Equal(t, "rspecish", j.Name()) +} + +func TestRspecish_Output(t *testing.T) { + var wg sync.WaitGroup + b := &bytes.Buffer{} + d, _ := time.ParseDuration("2s") + j := Rspecish{FakeDuration: d} + out := make(chan []resource.TestResult) + r := 1 + + go func() { + defer wg.Done() + wg.Add(1) + r = j.Output(b, out, time.Now(), util.OutputConfig{}) + }() + + out <- GetExampleTestResult() + + close(out) + wg.Wait() + expectedJson := `. + +Total Duration: 2.000s +Count: 1, Failed: 0, Skipped: 0 +` + assert.Equal(t, expectedJson, b.String()) + assert.Equal(t, 0, r) +} diff --git a/outputs/silent.go b/outputs/silent.go index 353ae4c..a925a3b 100644 --- a/outputs/silent.go +++ b/outputs/silent.go @@ -8,8 +8,13 @@ import ( "github.com/SimonBaeumer/goss/util" ) +// Silent represents the silent output type type Silent struct{} +// Name returns the name +func (r Silent) Name() string { return "silent" } + +// Output writes the actual output func (r Silent) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { diff --git a/outputs/tap.go b/outputs/tap.go index 4b6563b..d77a59f 100644 --- a/outputs/tap.go +++ b/outputs/tap.go @@ -10,8 +10,13 @@ import ( "github.com/SimonBaeumer/goss/util" ) +// Tap represents the tap output type type Tap struct{} +// Name returns the name +func (r Tap) Name() string { return "tap" } + +// Output writes the actual output func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { diff --git a/outputs/test_helper.go b/outputs/test_helper.go new file mode 100644 index 0000000..3e4ccb2 --- /dev/null +++ b/outputs/test_helper.go @@ -0,0 +1,20 @@ +package outputs + +import ( + "github.com/SimonBaeumer/goss/resource" + "time" +) + +func GetExampleTestResult() []resource.TestResult { + return []resource.TestResult{ + { + Title: "my title", + Duration: time.Duration(500), + Successful: true, + ResourceType: "resource type", + ResourceId: "my resource id", + Property: "a property", + Expected: []string{"expected"}, + }, + } +} diff --git a/resource/addr.go b/resource/addr.go index 476ec3a..0ddd69e 100644 --- a/resource/addr.go +++ b/resource/addr.go @@ -7,6 +7,7 @@ import ( const DefaultTimeoutMS = 500 +// Addr resource validates a addr, i.e. tcp://127.0.0.1:80 type Addr struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` diff --git a/resource/addr_test.go b/resource/addr_test.go index e50cd63..9672178 100644 --- a/resource/addr_test.go +++ b/resource/addr_test.go @@ -1,11 +1,11 @@ package resource import ( - "testing" - "github.com/SimonBaeumer/goss/util" "github.com/SimonBaeumer/goss/system/mock_system" + "github.com/SimonBaeumer/goss/util" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "testing" ) func TestAddr_NewAddr(t *testing.T) { diff --git a/resource/dns.go b/resource/dns.go index b9b36c4..2d44391 100644 --- a/resource/dns.go +++ b/resource/dns.go @@ -20,14 +20,16 @@ type DNS struct { } // ID returns the Host as the Identifier -func (d *DNS) ID() string { return d.Host } +func (d *DNS) ID() string { return d.Host } + // SetID sets the ID as Host func (d *DNS) SetID(id string) { d.Host = id } // GetTitle returns the title func (d *DNS) GetTitle() string { return d.Title } + /// GetMeta returns the meta of -func (d *DNS) GetMeta() meta { return d.Meta } +func (d *DNS) GetMeta() meta { return d.Meta } // Validate validates the given resource func (d *DNS) Validate(sys *system.System) []TestResult { diff --git a/resource/dns_test.go b/resource/dns_test.go index 77fbead..65233b4 100644 --- a/resource/dns_test.go +++ b/resource/dns_test.go @@ -1,132 +1,122 @@ package resource import ( - "github.com/SimonBaeumer/goss/system" - "github.com/SimonBaeumer/goss/util" - "github.com/stretchr/testify/assert" - "testing" + "github.com/SimonBaeumer/goss/system" + "github.com/SimonBaeumer/goss/util" + "github.com/SimonBaeumer/goss/util/goss_testing" + "github.com/stretchr/testify/assert" + "testing" ) var ( - QType = "TXT" - conf = util.Config{Timeout: 50} + QType = "TXT" + conf = util.Config{Timeout: 50} ) func TestNewDNS(t *testing.T) { - mockDns := MockSysDNS{} - dns, err := NewDNS(mockDns, conf) + mockDns := MockSysDNS{} + dns, err := NewDNS(mockDns, conf) - assert.Nil(t, err) - assert.Implements(t, new(Resource), dns) - assert.Equal(t, "TXT:google.com", dns.Host) + assert.Nil(t, err) + assert.Implements(t, new(Resource), dns) + assert.Equal(t, "TXT:google.com", dns.Host) } func TestNewDNS_WithoutQType(t *testing.T) { - QType = "" + QType = "" - mockDns := MockSysDNS{} - dns, err := NewDNS(mockDns, conf) + mockDns := MockSysDNS{} + dns, err := NewDNS(mockDns, conf) - assert.Nil(t, err) - assert.Implements(t, new(Resource), dns) - assert.Equal(t, "google.com", dns.Host) - assert.Equal(t, 50, dns.Timeout) - assert.True(t, dns.Resolvable.(bool)) + assert.Nil(t, err) + assert.Implements(t, new(Resource), dns) + assert.Equal(t, "google.com", dns.Host) + assert.Equal(t, 50, dns.Timeout) + assert.True(t, dns.Resolvable.(bool)) } func TestDNS_Validate(t *testing.T) { - addrs := convertToInterfaceSlice() + addrs := goss_testing.ConvertStringSliceToInterfaceSlice([]string{"localhost:53"}) - mockDns := MockSysDNS{} - dns, _ := NewDNS(mockDns, conf) - dns.Resolvable = true - dns.Host = "localhost" - dns.Addrs = addrs - dns.Timeout = 0 + mockDns := MockSysDNS{} + dns, _ := NewDNS(mockDns, conf) + dns.Resolvable = true + dns.Host = "localhost" + dns.Addrs = addrs + dns.Timeout = 0 - sys := system.System{} - sys.NewDNS = func (host string, sys *system.System, config util.Config) system.DNS { - return &MockSysDNS{Addr: []string{"localhost:53"}} - } + sys := system.System{} + sys.NewDNS = func(host string, sys *system.System, config util.Config) system.DNS { + return &MockSysDNS{Addr: []string{"localhost:53"}} + } - r := dns.Validate(&sys) + r := dns.Validate(&sys) - assert.Equal(t, 500, dns.Timeout, "Could not set default timeout if 0 was given") - assert.Len(t, r, 2) + assert.Equal(t, 500, dns.Timeout, "Could not set default timeout if 0 was given") + assert.Len(t, r, 2) - assert.True(t, r[0].Successful) - assert.Equal(t, "resolvable", r[0].Property) + assert.True(t, r[0].Successful) + assert.Equal(t, "resolvable", r[0].Property) - assert.True(t, r[1].Successful) - assert.Equal(t, "addrs", r[1].Property) + assert.True(t, r[1].Successful) + assert.Equal(t, "addrs", r[1].Property) } func TestDNS_ValidateFail(t *testing.T) { - addrs := convertToInterfaceSlice() + addrs := goss_testing.ConvertStringSliceToInterfaceSlice([]string{"localhost:53"}) - mockDns := MockSysDNS{} - dns, _ := NewDNS(mockDns, conf) - dns.Timeout = 50 - dns.Resolvable = true - dns.Host = "localhost" - dns.Addrs = addrs + mockDns := MockSysDNS{} + dns, _ := NewDNS(mockDns, conf) + dns.Timeout = 50 + dns.Resolvable = true + dns.Host = "localhost" + dns.Addrs = addrs - sys := system.System{} - sys.NewDNS = func(host string, sys *system.System, config util.Config) system.DNS { - return &MockSysDNS{Addr: []string{"ns.localhost"}} - } + sys := system.System{} + sys.NewDNS = func(host string, sys *system.System, config util.Config) system.DNS { + return &MockSysDNS{Addr: []string{"ns.localhost"}} + } - r := dns.Validate(&sys) + r := dns.Validate(&sys) - assert.Len(t, r, 2) - assert.True(t, r[0].Successful) - assert.Equal(t, "resolvable", r[0].Property) + assert.Len(t, r, 2) + assert.True(t, r[0].Successful) + assert.Equal(t, "resolvable", r[0].Property) - assert.False(t, r[1].Successful) - assert.Equal(t, "addrs", r[1].Property) - expectedHuman := `Expected + assert.False(t, r[1].Successful) + assert.Equal(t, "addrs", r[1].Property) + expectedHuman := `Expected <[]string | len:1, cap:1>: ["ns.localhost"] to contain element matching : localhost:53` - assert.Equal(t, expectedHuman, r[1].Human) -} - -func convertToInterfaceSlice() []interface{} { - // Create expected addrs as interface{} slice - // It is necessary to allocate the memory before, because []interface{} is of an unknown size - var expect = []string{"localhost:53"} - var addrs = make([]interface{}, len(expect)) - for i, char := range expect { - addrs[i] = char - } - return addrs + assert.Equal(t, expectedHuman, r[1].Human) } //MockSysDNS mocks the DNS system interface type MockSysDNS struct { - Addr []string + Addr []string } func (dns MockSysDNS) Addrs() ([]string, error) { - return dns.Addr, nil + return dns.Addr, nil } func (dns MockSysDNS) Resolvable() (bool, error) { - return true, nil + return true, nil } func (dns MockSysDNS) Exists() (bool, error) { - panic("implement me") + panic("implement me") } func (dns MockSysDNS) Server() string { - return "8.8.8.8" + return "8.8.8.8" } func (dns MockSysDNS) Qtype() string { - return QType + return QType } func (dns MockSysDNS) Host() string { - return "google.com" -} \ No newline at end of file + return "google.com" +} diff --git a/resource/http.go b/resource/http.go index 5338d5c..c240dae 100644 --- a/resource/http.go +++ b/resource/http.go @@ -3,30 +3,30 @@ package resource import ( "crypto/tls" "github.com/SimonBaeumer/goss/system" - "github.com/SimonBaeumer/goss/util" + "github.com/SimonBaeumer/goss/util" "log" "reflect" - "strings" - "time" + "strings" + "time" ) const TimeoutMS = 5000 type HTTP struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - HTTP string `json:"-" yaml:"-"` - Status matcher `json:"status" yaml:"status"` - AllowInsecure bool `json:"allow-insecure" yaml:"allow-insecure"` - NoFollowRedirects bool `json:"no-follow-redirects" yaml:"no-follow-redirects"` - Timeout int `json:"timeout" yaml:"timeout"` - Body []string `json:"body" yaml:"body"` - Username string `json:"username,omitempty" yaml:"username,omitempty"` - Password string `json:"password,omitempty" yaml:"password,omitempty"` - Headers map[string][]string `json:"headers,omitempty" yaml:"headers,omitempty"` - RequestHeaders map[string][]string `json:"request-headers,omitempty" yaml:"request-headers,omitempty"` - Cert string `json:"cert,omitempty" yaml:"cert,omitempty"` - Key string `json:"key,omitempty" yaml:"key,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + HTTP string `json:"-" yaml:"-"` + Status matcher `json:"status" yaml:"status"` + AllowInsecure bool `json:"allow-insecure" yaml:"allow-insecure"` + NoFollowRedirects bool `json:"no-follow-redirects" yaml:"no-follow-redirects"` + Timeout int `json:"timeout" yaml:"timeout"` + Body []string `json:"body" yaml:"body"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` + Password string `json:"password,omitempty" yaml:"password,omitempty"` + Headers map[string][]string `json:"headers,omitempty" yaml:"headers,omitempty"` + RequestHeaders map[string][]string `json:"request-headers,omitempty" yaml:"request-headers,omitempty"` + Cert string `json:"cert,omitempty" yaml:"cert,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` } func (u *HTTP) ID() string { return u.HTTP } @@ -75,9 +75,9 @@ func (u *HTTP) Validate(sys *system.System) []TestResult { } func (u *HTTP) loadClientCertificate() tls.Certificate { - if u.Cert == "" || u.Key == "" { - return tls.Certificate{} - } + if u.Cert == "" || u.Key == "" { + return tls.Certificate{} + } cert, err := tls.LoadX509KeyPair(u.Cert, u.Key) if err != nil { @@ -101,14 +101,13 @@ func NewHTTP(sysHTTP system.HTTP, config util.Config) (*HTTP, error) { AllowInsecure: config.AllowInsecure, NoFollowRedirects: config.NoFollowRedirects, Timeout: config.Timeout, - Username: config.Username, + Username: config.Username, Password: config.Password, - Headers: headers, + Headers: headers, } return u, err } - func validateHeader(res ResourceRead, property string, expectedHeaders map[string][]string, actualHeaders system.Header, skip bool) TestResult { id := res.ID() title := res.GetTitle() diff --git a/resource/http_test.go b/resource/http_test.go index 293abf4..e10acec 100644 --- a/resource/http_test.go +++ b/resource/http_test.go @@ -137,7 +137,7 @@ func Test_isNotInSlice(t *testing.T) { } func Test_ParseYAML(t *testing.T) { - configString := []byte(` + configString := []byte(` status: 200 allow-insecure: true no-follow-redirects: true diff --git a/resource/package.go b/resource/package.go index c60f397..51d5eb7 100644 --- a/resource/package.go +++ b/resource/package.go @@ -6,11 +6,12 @@ import ( ) type Package struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Name string `json:"-" yaml:"-"` - Installed matcher `json:"installed" yaml:"installed"` - Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + Name string `json:"-" yaml:"-"` + Installed matcher `json:"installed" yaml:"installed"` + Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"` + PackageManager string `json:"package-manager,omitempty" yaml:"package-manager,omitempty"` } func (p *Package) ID() string { return p.Name } @@ -21,7 +22,7 @@ func (p *Package) GetMeta() meta { return p.Meta } func (p *Package) Validate(sys *system.System) []TestResult { skip := false - sysPkg := sys.NewPackage(p.Name, sys, util.Config{}) + sysPkg := sys.NewPackage(p.Name, p.PackageManager) var results []TestResult results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip)) diff --git a/resource/package_test.go b/resource/package_test.go new file mode 100644 index 0000000..e303d52 --- /dev/null +++ b/resource/package_test.go @@ -0,0 +1,48 @@ +package resource + +import ( + "github.com/SimonBaeumer/goss/system" + "github.com/SimonBaeumer/goss/util" + "github.com/SimonBaeumer/goss/util/goss_testing" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewPackage(t *testing.T) { + pkg, _ := NewPackage(TestPackage{}, util.Config{}) + + assert.Equal(t, "test-pkg-manager", pkg.Name) + assert.True(t, pkg.Installed.(bool)) +} + +func TestPackage_Validate(t *testing.T) { + p := Package{ + Title: "vim", + Name: "vim", + Installed: false, + Versions: goss_testing.ConvertStringSliceToInterfaceSlice([]string{"1.0.0"}), + PackageManager: "deb", + } + + sys := &system.System{NewPackage: func(name string, pkg string) system.Package { + return TestPackage{} + }} + + r := p.Validate(sys) + + assert.False(t, r[0].Successful) + assert.Equal(t, "installed", r[0].Property) + + assert.True(t, r[1].Successful) + assert.Equal(t, "version", r[1].Property) +} + +type TestPackage struct{} + +func (p TestPackage) Name() string { return "test-pkg-manager" } + +func (p TestPackage) Exists() (bool, error) { return true, nil } + +func (p TestPackage) Installed() (bool, error) { return true, nil } + +func (p TestPackage) Versions() ([]string, error) { return []string{"1.0.0"}, nil } diff --git a/resource/resource_list.go b/resource/resource_list.go index 01f0869..6009abd 100644 --- a/resource/resource_list.go +++ b/resource/resource_list.go @@ -18,6 +18,7 @@ import ( //go:generate goimports -w resource_list.go resource_list.go type AddrMap map[string]*Addr + var BlacklistedAutoAddHeaders = [...]string{"Set-Cookie", "set-cookie", "Date", "date"} func (r AddrMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Addr, error) { @@ -621,7 +622,7 @@ func (ret *GroupMap) UnmarshalYAML(unmarshal func(v interface{}) error) error { type PackageMap map[string]*Package func (r PackageMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Package, error) { - sysres := sys.NewPackage(sr, sys, config) + sysres := sys.NewPackage(sr, "") res, err := NewPackage(sysres, config) if err != nil { return nil, err @@ -635,7 +636,7 @@ func (r PackageMap) AppendSysResource(sr string, sys *system.System, config util } func (r PackageMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Package, system.Package, bool) { - sysres := sys.NewPackage(sr, sys, util.Config{}) + sysres := sys.NewPackage(sr, "") // FIXME: Do we want to be silent about errors? res, _ := NewPackage(sysres, util.Config{}) if e, _ := sysres.Exists(); e != true { diff --git a/resource/resource_list_test.go b/resource/resource_list_test.go index 1b3db4a..e89706c 100644 --- a/resource/resource_list_test.go +++ b/resource/resource_list_test.go @@ -9,7 +9,6 @@ import ( const SuccessStatusCode = 200 - func TestAddrMap_AppendSysResource(t *testing.T) { conf := util.Config{} systemMock := &system.System{ diff --git a/resource/validate_test.go b/resource/validate_test.go index 8ab775d..efa348f 100644 --- a/resource/validate_test.go +++ b/resource/validate_test.go @@ -1,10 +1,10 @@ package resource import ( - "fmt" - "io" - "strings" - "testing" + "fmt" + "io" + "strings" + "testing" ) type FakeResource struct { diff --git a/serve.go b/serve.go index f89435b..f2b97b3 100644 --- a/serve.go +++ b/serve.go @@ -12,77 +12,68 @@ import ( "github.com/SimonBaeumer/goss/util" "github.com/fatih/color" "github.com/patrickmn/go-cache" - "github.com/urfave/cli" ) -func Serve(c *cli.Context) { - endpoint := c.String("endpoint") +//TODO: Maybe separating handler and server? +// HealthHandler creates a new handler for the health endpoint +type HealthHandler struct { + RunTimeConfig GossRunTime + GossConfig GossConfig + Sys *system.System + Outputer outputs.Outputer + Cache *cache.Cache + GossMu *sync.Mutex + ContentType string + MaxConcurrent int + ListenAddr string + FormatOptions []string +} + +// Serve creates a new endpoint and starts the http server +func (h *HealthHandler) Serve(endpoint string) { color.NoColor = true - cache := cache.New(c.Duration("cache"), 30*time.Second) - health := healthHandler{ - c: c, - gossConfig: getGossConfig(c), - sys: system.New(c), - outputer: getOutputer(c), - cache: cache, - gossMu: &sync.Mutex{}, - maxConcurrent: c.Int("max-concurrent"), - } - if c.String("format") == "json" { - health.contentType = "application/json" - } - http.Handle(endpoint, health) - listenAddr := c.String("listen-addr") - log.Printf("Starting to listen on: %s", listenAddr) - log.Fatal(http.ListenAndServe(c.String("listen-addr"), nil)) + http.Handle(endpoint, h) + log.Printf("Starting to listen on: %s", h.ListenAddr) + log.Fatal(http.ListenAndServe(h.ListenAddr, nil)) } type res struct { exitCode int b bytes.Buffer } -type healthHandler struct { - c *cli.Context - gossConfig GossConfig - sys *system.System - outputer outputs.Outputer - cache *cache.Cache - gossMu *sync.Mutex - contentType string - maxConcurrent int -} - -func (h healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { +//ServeHTTP fulfills the handler interface and is called as a handler on the +//health check request. +func (h HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { outputConfig := util.OutputConfig{ - FormatOptions: h.c.StringSlice("format-options"), + FormatOptions: h.FormatOptions, } log.Printf("%v: requesting health probe", r.RemoteAddr) var resp res - tmp, found := h.cache.Get("res") + tmp, found := h.Cache.Get("res") if found { resp = tmp.(res) } else { - h.gossMu.Lock() - defer h.gossMu.Unlock() - tmp, found := h.cache.Get("res") + h.GossMu.Lock() + defer h.GossMu.Unlock() + tmp, found := h.Cache.Get("res") if found { resp = tmp.(res) } else { - h.sys = system.New(h.c) - log.Printf("%v: Stale cache, running tests", r.RemoteAddr) + h.Sys = system.New() + log.Printf("%v: Stale Cache, running tests", r.RemoteAddr) iStartTime := time.Now() - out := validate(h.sys, h.gossConfig, h.maxConcurrent) + out := validate(h.Sys, h.GossConfig, h.MaxConcurrent) var b bytes.Buffer - exitCode := h.outputer.Output(&b, out, iStartTime, outputConfig) + exitCode := h.Outputer.Output(&b, out, iStartTime, outputConfig) resp = res{exitCode: exitCode, b: b} - h.cache.Set("res", resp, cache.DefaultExpiration) + h.Cache.Set("res", resp, cache.DefaultExpiration) } } - if h.contentType != "" { - w.Header().Set("Content-Type", h.contentType) + if h.ContentType != "" { + w.Header().Set("Content-Type", h.ContentType) } if resp.exitCode == 0 { resp.b.WriteTo(w) diff --git a/serve_test.go b/serve_test.go new file mode 100644 index 0000000..0a81247 --- /dev/null +++ b/serve_test.go @@ -0,0 +1,50 @@ +package goss + +import ( + "github.com/SimonBaeumer/goss/outputs" + "github.com/SimonBaeumer/goss/resource" + "github.com/patrickmn/go-cache" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" +) + +func TestHealthHandler_Serve(t *testing.T) { + req, err := http.NewRequest("GET", "/healthz", nil) + if err != nil { + t.Fatal(err) + } + + cmdResource := &resource.Command{ + Command: "echo hello", + Title: "echo hello", + ExitStatus: 0, + } + + h := HealthHandler{ + Cache: cache.New(time.Duration(50), time.Duration(50)), + Outputer: outputs.GetOutputer("documentation"), + MaxConcurrent: 1, + ListenAddr: "9999", + ContentType: "application/json", + GossMu: &sync.Mutex{}, + GossConfig: GossConfig{ + Commands: resource.CommandMap{"echo hello": cmdResource}, + }, + } + rr := httptest.NewRecorder() + + h.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("Health check failed!") + } + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Contains(t, rr.Body.String(), "Title: echo hello") + assert.Contains(t, rr.Body.String(), "Command: echo hello: exit-status: matches expectation: [0]") + assert.Contains(t, rr.Body.String(), "Count: 1, Failed: 0, Skipped: 0") +} diff --git a/store.go b/store.go index cec7266..a0df6bb 100644 --- a/store.go +++ b/store.go @@ -7,14 +7,10 @@ import ( "log" "os" "path/filepath" - "reflect" "sort" "strings" "gopkg.in/yaml.v2" - - "github.com/SimonBaeumer/goss/resource" - "github.com/urfave/cli" ) const ( @@ -92,7 +88,7 @@ func varsFromFile(varsFile string) (map[string]interface{}, error) { return vars, nil } -// Reads json byte array returning GossConfig +// ReadJSONData reads json byte array returning GossConfig func ReadJSONData(data []byte, detectFormat bool) GossConfig { if TemplateFilter != nil { data = TemplateFilter(data) @@ -115,15 +111,12 @@ func ReadJSONData(data []byte, detectFormat bool) GossConfig { return *gossConfig } -// Reads json file recursively returning string -func RenderJSON(c *cli.Context) string { - filePath := c.GlobalString("gossfile") - varsFile := c.GlobalString("vars") - debug = c.Bool("debug") - TemplateFilter = NewTemplateFilter(varsFile) - path := filepath.Dir(filePath) - OutStoreFormat = getStoreFormatFromFileName(filePath) - gossConfig := mergeJSONData(ReadJSON(filePath), 0, path) +// RenderJSON reads json file recursively returning string +func RenderJSON(gossfile *os.File, vars *os.File) string { + TemplateFilter = NewTemplateFilter(vars.Name()) + path := filepath.Dir(gossfile.Name()) + OutStoreFormat = getStoreFormatFromFileName(gossfile.Name()) + gossConfig := mergeJSONData(ReadJSON(gossfile.Name()), 0, path) b, err := marshal(gossConfig) if err != nil { @@ -138,7 +131,7 @@ func mergeJSONData(gossConfig GossConfig, depth int, path string) GossConfig { fmt.Println("Error: Max depth of 50 reached, possibly due to dependency loop in goss file") os.Exit(1) } - // Our return gossConfig + // Our return GossConfig ret := *NewGossConfig() ret = mergeGoss(ret, gossConfig) @@ -201,16 +194,6 @@ func WriteJSON(filePath string, gossConfig GossConfig) error { return nil } -func resourcePrint(fileName string, res resource.ResourceRead) { - resMap := map[string]resource.ResourceRead{res.ID(): res} - - oj, _ := marshal(resMap) - typ := reflect.TypeOf(res) - typs := strings.Split(typ.String(), ".")[1] - - fmt.Printf("Adding %s to '%s':\n\n%s\n\n", typs, fileName, string(oj)) -} - func marshal(gossConfig interface{}) ([]byte, error) { switch OutStoreFormat { case JSON: diff --git a/store_test.go b/store_test.go new file mode 100644 index 0000000..dd9ef4c --- /dev/null +++ b/store_test.go @@ -0,0 +1,123 @@ +package goss + +import ( + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "testing" +) + +const GossTestingEnvOS = "GOSS_TESTING_OS" + +func Test_RenderJSON(t *testing.T) { + err := os.Setenv(GossTestingEnvOS, "centos") + if err != nil { + panic(err.Error()) + } + defer os.Unsetenv(GossTestingEnvOS) + + tmpVars, err := ioutil.TempFile("", "example_tmp_vars_*.yaml") + if err != nil { + panic(err.Error()) + } + defer os.Remove(tmpVars.Name()) + + _, err = tmpVars.WriteString(getExampleVars()) + if err != nil { + panic(err.Error()) + } + + tmpGossfile, err := ioutil.TempFile("", "example_tmp_gossfile_*.yaml") + if err != nil { + panic(err.Error()) + } + defer os.Remove(tmpGossfile.Name()) + + _, err = tmpGossfile.WriteString(getExampleTemplate()) + if err != nil { + panic(err.Error()) + } + + result := RenderJSON(tmpGossfile, tmpVars) + + assert.Equal(t, getExpecetd(), result) +} + +func getExampleVars() string { + return ` + centos: + packages: + kernel: + - "4.9.11-centos" + - "4.9.11-centos2" + debian: + packages: + kernel: + - "4.9.11-debian" + - "4.9.11-debian2" + users: + - user1 + - user2 + ` +} + +func getExampleTemplate() string { + return ` + package: + # Looping over a variables defined in a vars.yaml using $OS environment variable as a lookup key + {{range $name, $vers := index .Vars .Env.GOSS_TESTING_OS "packages"}} + {{$name}}: + installed: true + versions: + {{range $vers}} + - {{.}} + {{end}} + {{end}} + + # This test is only when the OS environment variable matches the pattern + {{if .Env.GOSS_TESTING_OS | regexMatch "[Cc]ent(OS|os)"}} + libselinux: + installed: true + {{end}} + + # Loop over users + user: + {{range .Vars.users}} + {{.}}: + exists: true + groups: + - {{.}} + home: /home/{{.}} + shell: /bin/bash + {{end}} + + + package: + {{if eq .Env.GOSS_TESTING_OS "centos"}} + # This test is only when $OS environment variable is set to "centos" + libselinux: + installed: true + {{end}} + ` +} + +func getExpecetd() string { + expected := `package: + libselinux: + installed: true +user: + user1: + exists: true + groups: + - user1 + home: /home/user1 + shell: /bin/bash + user2: + exists: true + groups: + - user2 + home: /home/user2 + shell: /bin/bash +` + return expected +} diff --git a/system/dns.go b/system/dns.go index 6649d05..970479f 100644 --- a/system/dns.go +++ b/system/dns.go @@ -64,6 +64,7 @@ func (d *DefDNS) Server() string { func (d *DefDNS) Qtype() string { return d.qtype } + // setup executes the dns lookup func (d *DefDNS) setup() error { if d.loaded { diff --git a/system/dns_test.go b/system/dns_test.go index 2252e0b..8fac45c 100644 --- a/system/dns_test.go +++ b/system/dns_test.go @@ -1,76 +1,76 @@ package system import ( - "github.com/SimonBaeumer/goss/util" - "github.com/stretchr/testify/assert" - "testing" + "github.com/SimonBaeumer/goss/util" + "github.com/stretchr/testify/assert" + "testing" ) func TestNewDefDNS(t *testing.T) { - dns := NewDefDNS("google.com", &System{}, util.Config{Timeout: 50, Server: "8.8.8.8"}) + dns := NewDefDNS("google.com", &System{}, util.Config{Timeout: 50, Server: "8.8.8.8"}) - assert.Implements(t, new(DNS), dns) - assert.Equal(t, "8.8.8.8", dns.Server()) + assert.Implements(t, new(DNS), dns) + assert.Equal(t, "8.8.8.8", dns.Server()) } func TestNewDefDNS_WithQueryType(t *testing.T) { - dns := NewDefDNS("TXT:google.com", &System{}, util.Config{}) + dns := NewDefDNS("TXT:google.com", &System{}, util.Config{}) - assert.Implements(t, new(DNS), dns) - assert.Equal(t, "TXT", dns.Qtype()) - assert.Equal(t, "google.com", dns.Host()) + assert.Implements(t, new(DNS), dns) + assert.Equal(t, "TXT", dns.Qtype()) + assert.Equal(t, "google.com", dns.Host()) } func TestAddr(t *testing.T) { - dns := NewDefDNS("localhost", &System{}, util.Config{Timeout: 50}) + dns := NewDefDNS("localhost", &System{}, util.Config{Timeout: 200}) - r, err := dns.Resolvable() - assert.Nil(t, err) - assert.True(t, r) + r, err := dns.Resolvable() + assert.Nil(t, err) + assert.True(t, r) } func TestAddr_WithServer(t *testing.T) { - dns := NewDefDNS("google.com", &System{}, util.Config{Timeout: 150, Server: "8.8.8.8"}) + dns := NewDefDNS("google.com", &System{}, util.Config{Timeout: 150, Server: "8.8.8.8"}) - r, err := dns.Resolvable() - assert.Nil(t, err) - assert.True(t, r) + r, err := dns.Resolvable() + assert.Nil(t, err) + assert.True(t, r) } func TestDNSLookup(t *testing.T) { - dns := NewDefDNS("localhost", &System{}, util.Config{Timeout: 500}) + dns := NewDefDNS("localhost", &System{}, util.Config{Timeout: 500}) - r, err := dns.Resolvable() - addr, _ := dns.Addrs() + r, err := dns.Resolvable() + addr, _ := dns.Addrs() - assert.Nil(t, err) - assert.True(t, r) - assert.Equal(t, []string{"127.0.0.1"}, addr) + assert.Nil(t, err) + assert.True(t, r) + assert.Equal(t, []string{"127.0.0.1"}, addr) } func TestDefDNS_Exists(t *testing.T) { - dns := NewDefDNS("", &System{}, util.Config{}) - r, _ := dns.Exists() - assert.False(t, r) + dns := NewDefDNS("", &System{}, util.Config{}) + r, _ := dns.Exists() + assert.False(t, r) } func TestDNSLookup_ShouldFail(t *testing.T) { - dns := NewDefDNS("SRV:s-baeumer.de", &System{}, util.Config{Timeout: 500}) + dns := NewDefDNS("SRV:s-baeumer.de", &System{}, util.Config{Timeout: 500}) - r, err := dns.Resolvable() - assert.Nil(t, err) - assert.False(t, r) + r, err := dns.Resolvable() + assert.Nil(t, err) + assert.False(t, r) } func TestDNSLook_WithInvalidAddress(t *testing.T) { - dns := NewDefDNS("thisdomaindoesnotexist123.com", &System{}, util.Config{Timeout: 50}) - r, _ := dns.Resolvable() - assert.False(t, r) + dns := NewDefDNS("thisdomaindoesnotexist123.com", &System{}, util.Config{Timeout: 50}) + r, _ := dns.Resolvable() + assert.False(t, r) } func TestDNSLookupWithTimeout(t *testing.T) { - dns := NewDefDNS("SRV:s-baeumer.de", &System{}, util.Config{Timeout: 0}) - r, err := dns.Resolvable() - assert.Equal(t, "DNS lookup timed out (0s)", err.Error()) - assert.False(t, r) + dns := NewDefDNS("SRV:s-baeumer.de", &System{}, util.Config{Timeout: 0}) + r, err := dns.Resolvable() + assert.Equal(t, "DNS lookup timed out (0s)", err.Error()) + assert.False(t, r) } diff --git a/system/http.go b/system/http.go index 89edd00..3f22d3f 100644 --- a/system/http.go +++ b/system/http.go @@ -44,7 +44,7 @@ func NewDefHTTP(http string, system *System, config util.Config) HTTP { allowInsecure: config.AllowInsecure, noFollowRedirects: config.NoFollowRedirects, Timeout: config.Timeout, - Username: config.Username, + Username: config.Username, Password: config.Password, RequestHeaders: config.RequestHeaders, ClientCertificate: config.Certificate, @@ -58,12 +58,11 @@ func (u *DefHTTP) setup() error { } u.loaded = true - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ InsecureSkipVerify: u.allowInsecure, - Certificates: []tls.Certificate{u.ClientCertificate}, - }, + Certificates: []tls.Certificate{u.ClientCertificate}, + }, DisableKeepAlives: true, } client := &http.Client{ @@ -154,4 +153,4 @@ func (u *DefHTTP) Headers() (Header, error) { } return headers, nil -} \ No newline at end of file +} diff --git a/system/kernel_param.go b/system/kernel_param.go index 7ff8cc3..dd9b7a5 100644 --- a/system/kernel_param.go +++ b/system/kernel_param.go @@ -1,8 +1,8 @@ package system import ( - "github.com/achanda/go-sysctl" "github.com/SimonBaeumer/goss/util" + "github.com/achanda/go-sysctl" ) type KernelParam interface { diff --git a/system/package.go b/system/package.go index c232290..db551ac 100644 --- a/system/package.go +++ b/system/package.go @@ -34,3 +34,46 @@ func (p *NullPackage) Installed() (bool, error) { func (p *NullPackage) Versions() ([]string, error) { return nil, ErrNullPackage } + +// DetectPackageManager attempts to detect whether or not the system is using +// "deb", "rpm", "apk", or "pacman" package managers. It first attempts to +// detect the distro. If that fails, it falls back to finding package manager +// executables. If that fails, it returns the empty string. +func DetectPackageManager() string { + switch DetectDistro() { + case "ubuntu": + return "deb" + case "redhat": + return "rpm" + case "alpine": + return "apk" + case "arch": + return "pacman" + case "debian": + return "deb" + } + for _, manager := range []string{"deb", "rpm", "apk", "pacman"} { + if HasCommand(manager) { + return manager + } + } + return "" +} + +// NewPackage is the constructor method which creates the correct package manager +// If pkgManager is empty the package manager will be automatically detected +func NewPackage(name string, pkgManager string) Package { + if pkgManager != "deb" && pkgManager != "apk" && pkgManager != "pacman" && pkgManager != "rpm" { + pkgManager = DetectPackageManager() + } + switch pkgManager { + case "deb": + return NewDebPackage(name) + case "apk": + return NewAlpinePackage(name) + case "pacman": + return NewPacmanPackage(name) + default: + return NewRpmPackage(name) + } +} diff --git a/system/package_alpine.go b/system/package_alpine.go index ff000b9..d231297 100644 --- a/system/package_alpine.go +++ b/system/package_alpine.go @@ -14,7 +14,7 @@ type AlpinePackage struct { installed bool } -func NewAlpinePackage(name string, system *System, config util.Config) Package { +func NewAlpinePackage(name string) Package { return &AlpinePackage{name: name} } diff --git a/system/package_deb.go b/system/package_deb.go index c229373..61dd6cb 100644 --- a/system/package_deb.go +++ b/system/package_deb.go @@ -14,7 +14,7 @@ type DebPackage struct { installed bool } -func NewDebPackage(name string, system *System, config util.Config) Package { +func NewDebPackage(name string) Package { return &DebPackage{name: name} } diff --git a/system/package_pacman.go b/system/package_pacman.go index 37f5138..58ed57f 100644 --- a/system/package_pacman.go +++ b/system/package_pacman.go @@ -7,6 +7,7 @@ import ( "github.com/SimonBaeumer/goss/util" ) +//PackmanPackage represents a package inside the pacman manager type PacmanPackage struct { name string versions []string @@ -14,7 +15,8 @@ type PacmanPackage struct { installed bool } -func NewPacmanPackage(name string, system *System, config util.Config) Package { +//NewPacmanPackage creates a new pacman manager +func NewPacmanPackage(name string) Package { return &PacmanPackage{name: name} } @@ -34,18 +36,22 @@ func (p *PacmanPackage) setup() { p.versions = []string{strings.Fields(cmd.Stdout.String())[1]} } +// Name returns the name of the package func (p *PacmanPackage) Name() string { return p.name } +// Exists returns if the package is installed func (p *PacmanPackage) Exists() (bool, error) { return p.Installed() } +// Installed will check and returns if the package is installed func (p *PacmanPackage) Installed() (bool, error) { p.setup() return p.installed, nil } +// Versions returns all installed versions of the package func (p *PacmanPackage) Versions() ([]string, error) { p.setup() if len(p.versions) == 0 { diff --git a/system/package_rpm.go b/system/package_rpm.go index 8a3faf4..0a9d034 100644 --- a/system/package_rpm.go +++ b/system/package_rpm.go @@ -14,7 +14,7 @@ type RpmPackage struct { installed bool } -func NewRpmPackage(name string, system *System, config util.Config) Package { +func NewRpmPackage(name string) Package { return &RpmPackage{name: name} } diff --git a/system/package_test.go b/system/package_test.go new file mode 100644 index 0000000..257e6c2 --- /dev/null +++ b/system/package_test.go @@ -0,0 +1,25 @@ +package system + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewPackage(t *testing.T) { + deb := NewPackage("package", "deb") + rpm := NewPackage("package", "rpm") + pac := NewPackage("package", "pacman") + apk := NewPackage("package", "apk") + + assert.Implements(t, new(Package), deb) + assert.IsType(t, &DebPackage{}, deb) + + assert.Implements(t, new(Package), rpm) + assert.IsType(t, &RpmPackage{}, rpm) + + assert.Implements(t, new(Package), pac) + assert.IsType(t, &PacmanPackage{}, pac) + + assert.Implements(t, new(Package), apk) + assert.IsType(t, &AlpinePackage{}, apk) +} diff --git a/system/port.go b/system/port.go index 084d2e3..ce328f1 100644 --- a/system/port.go +++ b/system/port.go @@ -4,8 +4,8 @@ import ( "strconv" "strings" - "github.com/aelsabbahy/GOnetstat" "github.com/SimonBaeumer/goss/util" + "github.com/aelsabbahy/GOnetstat" ) type Port interface { diff --git a/system/process.go b/system/process.go index ac11bb8..b9f51e1 100644 --- a/system/process.go +++ b/system/process.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - "github.com/aelsabbahy/go-ps" "github.com/SimonBaeumer/goss/util" + "github.com/aelsabbahy/go-ps" ) type Process interface { diff --git a/system/system.go b/system/system.go index 7276ba2..517e2ce 100644 --- a/system/system.go +++ b/system/system.go @@ -7,11 +7,10 @@ import ( "os/exec" "sync" + util2 "github.com/SimonBaeumer/goss/util" "github.com/aelsabbahy/GOnetstat" // This needs a better name "github.com/aelsabbahy/go-ps" - util2 "github.com/SimonBaeumer/goss/util" - "github.com/urfave/cli" ) type Resource interface { @@ -20,7 +19,7 @@ type Resource interface { // System holds all constructor functions for each type System struct { - NewPackage func(string, *System, util2.Config) Package + NewPackage func(string, string) Package NewFile func(string, *System, util2.Config) File NewAddr func(string, *System, util2.Config) Addr NewPort func(string, *System, util2.Config) Port @@ -55,8 +54,10 @@ func (s *System) ProcMap() map[string][]ps.Process { return s.procMap } -func New(c *cli.Context) *System { +//New creates the system object which holds all constructors for the system packages +func New() *System { sys := &System{ + NewPackage: NewPackage, NewFile: NewDefFile, NewAddr: NewDefAddr, NewPort: NewDefPort, @@ -72,28 +73,9 @@ func New(c *cli.Context) *System { NewHTTP: NewDefHTTP, } sys.detectService() - sys.detectPackage(c) return sys } -// DetectPackage adds the correct package creation function to a System struct -func (sys *System) detectPackage(c *cli.Context) { - p := c.GlobalString("package") - if p != "deb" && p != "apk" && p != "pacman" && p != "rpm" { - p = DetectPackageManager() - } - switch p { - case "deb": - sys.NewPackage = NewDebPackage - case "apk": - sys.NewPackage = NewAlpinePackage - case "pacman": - sys.NewPackage = NewPacmanPackage - default: - sys.NewPackage = NewRpmPackage - } -} - // DetectService adds the correct service creation function to a System struct func (sys *System) detectService() { switch DetectService() { @@ -108,31 +90,6 @@ func (sys *System) detectService() { } } -// DetectPackageManager attempts to detect whether or not the system is using -// "deb", "rpm", "apk", or "pacman" package managers. It first attempts to -// detect the distro. If that fails, it falls back to finding package manager -// executables. If that fails, it returns the empty string. -func DetectPackageManager() string { - switch DetectDistro() { - case "ubuntu": - return "deb" - case "redhat": - return "rpm" - case "alpine": - return "apk" - case "arch": - return "pacman" - case "debian": - return "deb" - } - for _, manager := range []string{"deb", "rpm", "apk", "pacman"} { - if HasCommand(manager) { - return manager - } - } - return "" -} - // DetectService attempts to detect what kind of service management the system // is using, "systemd", "upstart", "alpineinit", or "init". It looks for systemctl // command to detect systemd, and falls back on DetectDistro otherwise. If it can't diff --git a/template.go b/template.go index 97837b3..6d02bf8 100644 --- a/template.go +++ b/template.go @@ -11,10 +11,12 @@ import ( "text/template" ) +// mkSlice is able to create loops in templates func mkSlice(args ...interface{}) []interface{} { return args } +// readFile reads a file from inside a template func readFile(f string) (string, error) { b, err := ioutil.ReadFile(f) if err != nil { @@ -49,6 +51,7 @@ var funcMap = map[string]interface{}{ "regexMatch": regexMatch, } +// NewTemplateFilter creates a new filter with template extensions func NewTemplateFilter(varsFile string) func([]byte) []byte { vars, err := varsFromFile(varsFile) if err != nil { diff --git a/template_test.go b/template_test.go new file mode 100644 index 0000000..9501bba --- /dev/null +++ b/template_test.go @@ -0,0 +1,86 @@ +package goss + +import ( + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "testing" +) + +func Test_NewTemplateFilter_Variable(t *testing.T) { + vars, err := ioutil.TempFile("", "*_vars.yaml") + if err != nil { + panic(err.Error()) + } + defer os.Remove(vars.Name()) + + _, err = vars.WriteString("test: testing") + if err != nil { + panic(err.Error()) + } + + content := []byte(`variable: {{.Vars.test}}`) + + filter := NewTemplateFilter(vars.Name()) + result := filter(content) + + assert.Equal(t, "variable: testing", string(result)) +} + +func Test_NewTemplateFilter_Env(t *testing.T) { + err := os.Setenv("GOSS_TEST_ENV", "env testing") + if err != nil { + panic(err.Error()) + } + defer os.Unsetenv("template_goss_test_env") + + content := []byte(`environment: {{.Env.GOSS_TEST_ENV}}`) + + filter := NewTemplateFilter("") + result := filter(content) + + assert.Equal(t, "environment: env testing", string(result)) +} + +func Test_NewTemplateFilter_mkSlice(t *testing.T) { + content := []byte(`{{- range mkSlice "test1" "test2" "test3"}}{{.}}{{end}}`) + + filter := NewTemplateFilter("") + result := filter(content) + + assert.Equal(t, "test1test2test3", string(result)) +} + +func Test_NewTemplateFilter_readFile(t *testing.T) { + tmpFile, err := ioutil.TempFile("", "read_file_temp") + if err != nil { + panic(err.Error()) + } + defer os.Remove(tmpFile.Name()) + tmpFile.WriteString("test read file from template") + + content := []byte(`{{readFile "` + tmpFile.Name() + `"}}`) + + filter := NewTemplateFilter("") + result := filter(content) + + assert.Equal(t, "test read file from template", string(result)) +} + +func Test_NewTemplateFilter_regexMatch(t *testing.T) { + content := []byte(`{{if "centos" | regexMatch "[Cc]ent(OS|os)"}}detected regex{{end}}`) + + filter := NewTemplateFilter("") + result := filter(content) + + assert.Equal(t, "detected regex", string(result)) +} + +func Test_NewTemplateFilter_regexMatch_fail(t *testing.T) { + content := []byte(`{{if "ubuntu" | regexMatch "[Cc]ent(OS|os)"}}detected regex{{else}}no match{{end}}`) + + filter := NewTemplateFilter("") + result := filter(content) + + assert.Equal(t, "no match", string(result)) +} diff --git a/util/command.go b/util/command.go index 92a03e8..3f9d543 100644 --- a/util/command.go +++ b/util/command.go @@ -7,6 +7,7 @@ import ( "syscall" ) +// Command represents a command which can be executed type Command struct { name string Cmd *exec.Cmd @@ -15,6 +16,7 @@ type Command struct { Status int } +// NewCommand creates a command func NewCommand(name string, arg ...string) *Command { //fmt.Println(arg) command := new(Command) @@ -23,6 +25,7 @@ func NewCommand(name string, arg ...string) *Command { return command } +// Run executes the command and writes the results to its properties func (c *Command) Run() error { c.Cmd.Stdout = &c.Stdout c.Cmd.Stderr = &c.Stderr diff --git a/util/command_test.go b/util/command_test.go new file mode 100644 index 0000000..238d408 --- /dev/null +++ b/util/command_test.go @@ -0,0 +1,22 @@ +package util + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewCommand(t *testing.T) { + cmd := NewCommand("/bin/sh") + assert.Equal(t, "/bin/sh", cmd.name) + assert.Equal(t, "", cmd.Stdout.String()) +} + +func TestCommand_Run(t *testing.T) { + cmd := NewCommand("/bin/sh", "-c", "echo test") + err := cmd.Run() + + assert.Nil(t, err) + assert.Equal(t, "test\n", cmd.Stdout.String()) + assert.Equal(t, "", cmd.Stderr.String()) + assert.Equal(t, 0, cmd.Status) +} diff --git a/util/config.go b/util/config.go index 2c7b763..cc8601b 100644 --- a/util/config.go +++ b/util/config.go @@ -24,7 +24,6 @@ type Config struct { } type Request struct { - } type OutputConfig struct { diff --git a/util/goss_testing/helper.go b/util/goss_testing/helper.go new file mode 100644 index 0000000..20d2285 --- /dev/null +++ b/util/goss_testing/helper.go @@ -0,0 +1,11 @@ +package goss_testing + +//ConvertStringSliceToInterfaceSlice is a helper function to match +// system interfaces +func ConvertStringSliceToInterfaceSlice(strings []string) []interface{} { + var iStrings = make([]interface{}, len(strings)) + for i, char := range strings { + iStrings[i] = char + } + return iStrings +} diff --git a/validate.go b/validate.go index 19f40b4..b92d088 100644 --- a/validate.go +++ b/validate.go @@ -2,9 +2,8 @@ package goss import ( "fmt" - "io/ioutil" + "io" "os" - "path/filepath" "runtime" "sync" "time" @@ -14,89 +13,57 @@ import ( "github.com/SimonBaeumer/goss/system" "github.com/SimonBaeumer/goss/util" "github.com/fatih/color" - "github.com/urfave/cli" ) -func getGossConfig(c *cli.Context) GossConfig { - // handle stdin - var fh *os.File - var path, source string - var gossConfig GossConfig - TemplateFilter = NewTemplateFilter(c.GlobalString("vars")) - specFile := c.GlobalString("gossfile") - if specFile == "-" { - source = "STDIN" - fh = os.Stdin - data, err := ioutil.ReadAll(fh) - if err != nil { - fmt.Printf("Error: %v\n", err) - os.Exit(1) - } - OutStoreFormat = getStoreFormatFromData(data) - gossConfig = ReadJSONData(data, true) - } else { - source = specFile - path = filepath.Dir(specFile) - OutStoreFormat = getStoreFormatFromFileName(specFile) - gossConfig = ReadJSON(specFile) - } - - gossConfig = mergeJSONData(gossConfig, 0, path) - - if len(gossConfig.Resources()) == 0 { - fmt.Printf("Error: found 0 tests, source: %v\n", source) - os.Exit(1) - } - return gossConfig -} - -func getOutputer(c *cli.Context) outputs.Outputer { - if c.Bool("no-color") { - color.NoColor = true - } - if c.Bool("color") { - color.NoColor = false - } - return outputs.GetOutputer(c.String("format")) +type Validator struct { + GossConfig GossConfig + RetryTimeout time.Duration + Sleep time.Duration + FormatOptions []string + Outputer outputs.Outputer + MaxConcurrent int //Separating concurrency and validation, irritating atm... + OutputWriter io.Writer } // Validate validation runtime -func Validate(c *cli.Context, startTime time.Time) { +func (v *Validator) Validate(startTime time.Time) int { + if v.OutputWriter == nil { + v.OutputWriter = os.Stdout + } outputConfig := util.OutputConfig{ - FormatOptions: c.StringSlice("format-options"), + FormatOptions: v.FormatOptions, } - gossConfig := getGossConfig(c) - sys := system.New(c) - outputer := getOutputer(c) + sys := system.New() - sleep := c.Duration("sleep") - retryTimeout := c.Duration("retry-timeout") i := 1 for { iStartTime := time.Now() - out := validate(sys, gossConfig, c.Int("max-concurrent")) - exitCode := outputer.Output(os.Stdout, out, iStartTime, outputConfig) - if retryTimeout == 0 || exitCode == 0 { - os.Exit(exitCode) + + out := validate(sys, v.GossConfig, v.MaxConcurrent) + exitCode := v.Outputer.Output(v.OutputWriter, out, iStartTime, outputConfig) + if v.RetryTimeout == 0 || exitCode == 0 { + return exitCode } + elapsed := time.Since(startTime) - if elapsed+sleep > retryTimeout { - color.Red("\nERROR: Timeout of %s reached before tests entered a passing state", retryTimeout) - os.Exit(3) + if elapsed+v.Sleep > v.RetryTimeout { + color.Red("\nERROR: Timeout of %s reached before tests entered a passing state", v.RetryTimeout) + return exitCode } - color.Red("Retrying in %s (elapsed/timeout time: %.3fs/%s)\n\n\n", sleep, elapsed.Seconds(), retryTimeout) - // Reset cache - sys = system.New(c) - time.Sleep(sleep) + color.Red("Retrying in %s (elapsed/timeout time: %.3fs/%s)\n\n\n", v.Sleep, elapsed.Seconds(), v.RetryTimeout) + + // Reset Cache + sys = system.New() + time.Sleep(v.Sleep) i++ fmt.Printf("Attempt #%d:\n", i) } } func validate(sys *system.System, gossConfig GossConfig, maxConcurrent int) <-chan []resource.TestResult { - out := make(chan []resource.TestResult) + out := make(chan []resource.TestResult) in := make(chan resource.Resource) // Send resources to input channel @@ -120,7 +87,6 @@ func validate(sys *system.System, gossConfig GossConfig, maxConcurrent int) <-ch for res := range in { out <- res.Validate(sys) } - }() } diff --git a/validate_test.go b/validate_test.go new file mode 100644 index 0000000..45137c8 --- /dev/null +++ b/validate_test.go @@ -0,0 +1,41 @@ +package goss + +import ( + "bytes" + "github.com/SimonBaeumer/goss/outputs" + "github.com/SimonBaeumer/goss/resource" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestValidator_Validate(t *testing.T) { + cmdRes := &resource.Command{Title: "echo hello", Command: "echo hello", ExitStatus: 0} + fileRes := &resource.File{Title: "/tmp", Path: "/tmp", Filetype: "directory", Exists: true} + addrRes := &resource.Addr{Title: "tcp://google.com:443", Address: "tcp://google.com:443", Reachable: true} + httpRes := &resource.HTTP{Title: "https://google.com", HTTP: "https://google.com", Status: 200} + userRes := &resource.User{Title: "root", Username: "root", Exists: true} + groupRes := &resource.Group{Title: "root", Groupname: "root", Exists: true} + dnsRes := &resource.DNS{Title: "A:google.com", Host: "A:google.com", Resolvable: true} + + w := &bytes.Buffer{} + v := Validator{ + GossConfig: GossConfig{ + Commands: resource.CommandMap{"echo hello": cmdRes}, + Files: resource.FileMap{"/tmp": fileRes}, + Addrs: resource.AddrMap{"127.0.0.1": addrRes}, + HTTPs: resource.HTTPMap{"https://google.com": httpRes}, + Users: resource.UserMap{"root": userRes}, + Groups: resource.GroupMap{"root": groupRes}, + DNS: resource.DNSMap{"A:https://google.com": dnsRes}, + }, + MaxConcurrent: 1, + Outputer: outputs.GetOutputer("documentation"), + OutputWriter: w, + } + + r := v.Validate(time.Now()) + + assert.Equal(t, 0, r) + assert.Contains(t, w.String(), "Count: 8, Failed: 0, Skipped: 0") +}