├── development ├── debian │ ├── .gitignore │ └── Vagrantfile ├── README.md ├── build_images.sh └── push_images.sh ├── integration-tests ├── goss │ ├── hellogoss.txt │ ├── goss-wait.yaml │ ├── alpine3 │ │ ├── goss-aa-expected.yaml │ │ ├── goss.yaml │ │ ├── goss-expected-q.yaml │ │ └── goss-expected.yaml │ ├── vars.yaml │ ├── centos7 │ │ ├── goss-aa-expected.yaml │ │ ├── goss.yaml │ │ ├── goss-expected-q.yaml │ │ └── goss-expected.yaml │ ├── wheezy │ │ ├── goss-aa-expected.yaml │ │ ├── goss.yaml │ │ ├── goss-expected-q.yaml │ │ └── goss-expected.yaml │ ├── goss-service.yaml │ ├── precise │ │ ├── goss-aa-expected.yaml │ │ ├── goss.yaml │ │ ├── goss-expected-q.yaml │ │ └── goss-expected.yaml │ ├── arch │ │ └── goss.yaml │ ├── generate_goss.sh │ └── goss-shared.yaml ├── Dockerfile_arch.md5 ├── Dockerfile_alpine3.md5 ├── Dockerfile_centos7.md5 ├── Dockerfile_precise.md5 ├── Dockerfile_wheezy.md5 ├── Dockerfile_arch ├── Dockerfile_wheezy ├── Dockerfile_alpine3 ├── Dockerfile_precise ├── Dockerfile_centos7 └── test.sh ├── system ├── service.go ├── gossfile.go ├── kernel_param.go ├── group.go ├── package.go ├── package_rpm.go ├── package_pacman.go ├── system_test.go ├── addr.go ├── process.go ├── package_alpine.go ├── package_deb.go ├── interface.go ├── service_systemd.go ├── service_init.go ├── service_upstart.go ├── mount.go ├── command.go ├── http.go ├── user.go ├── port.go ├── system.go └── file.go ├── .gitignore ├── outputs ├── silent.go ├── nagios.go ├── nagios_verbose.go ├── tap.go ├── rspecish.go ├── documentation.go ├── json.go ├── junit.go └── outputs.go ├── glide.yaml ├── resource ├── gossfile.go ├── process.go ├── kernel_param.go ├── addr.go ├── resource.go ├── service.go ├── port.go ├── package.go ├── interface.go ├── group.go ├── dns.go ├── http.go ├── mount.go ├── command.go ├── user.go ├── matching.go ├── resource_list_genny.go ├── file.go ├── validate_test.go ├── gomega_test.go └── gomega.go ├── install.sh ├── util ├── command.go └── config.go ├── .travis.yml ├── template.go ├── glide.lock ├── serve.go ├── validate.go ├── extras └── dgoss │ ├── dgoss │ └── README.md ├── Makefile ├── goss_config.go ├── store.go ├── add.go └── README.md /development/debian/.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | -------------------------------------------------------------------------------- /integration-tests/goss/hellogoss.txt: -------------------------------------------------------------------------------- 1 | Goss Rocks!! 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_arch.md5: -------------------------------------------------------------------------------- 1 | f73a8fa9e9c0940ce2bfc09c13c3aaf5 Dockerfile_arch 2 | -------------------------------------------------------------------------------- /development/README.md: -------------------------------------------------------------------------------- 1 | # Random development scripts 2 | 3 | Nothing to see here, carry on :) 4 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_alpine3.md5: -------------------------------------------------------------------------------- 1 | 9cc0707454b2455422e5e8b6a6c74edb Dockerfile_alpine3 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_centos7.md5: -------------------------------------------------------------------------------- 1 | 4df4f4463945687a115a13b11649dfb0 Dockerfile_centos7 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_precise.md5: -------------------------------------------------------------------------------- 1 | 82365f88b801fe4b1073a3e28a30e2a5 Dockerfile_precise 2 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_wheezy.md5: -------------------------------------------------------------------------------- 1 | fe21d74d27c0dc3a013bedadb05e75d5 Dockerfile_wheezy 2 | -------------------------------------------------------------------------------- /integration-tests/goss/goss-wait.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | addr: 3 | tcp://localhost:80: 4 | reachable: true 5 | timeout: 500 6 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_arch: -------------------------------------------------------------------------------- 1 | FROM base/archlinux 2 | MAINTAINER @siddharthist 3 | 4 | RUN ln -s /does_not_exist /foo && \ 5 | chmod 700 ~root 6 | -------------------------------------------------------------------------------- /integration-tests/goss/alpine3/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | apache2: 3 | installed: true 4 | versions: 5 | - 2.4.23-r1 6 | service: 7 | apache2: 8 | enabled: true 9 | running: true 10 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_wheezy: -------------------------------------------------------------------------------- 1 | FROM debian:wheezy 2 | MAINTAINER Ahmed 3 | 4 | RUN apt-get update && apt-get install -y apache2=2.2.22-13+deb7u7 chkconfig vim-tiny ca-certificates && apt-get remove -y vim-tiny && apt-get clean 5 | 6 | RUN chkconfig apache2 on 7 | -------------------------------------------------------------------------------- /integration-tests/goss/vars.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | alpine3: 3 | packages: 4 | apache2: "2.4.23-r1" 5 | arch: 6 | packages: 7 | centos7: 8 | packages: 9 | httpd: "2.4.6" 10 | precise: 11 | packages: 12 | apache2: "2.2.22-1ubuntu1.11" 13 | wheezy: 14 | packages: 15 | apache2: "2.2.22-13+deb7u7" 16 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_alpine3: -------------------------------------------------------------------------------- 1 | FROM gliderlabs/alpine:3.3 2 | MAINTAINER Ahmed 3 | 4 | # install apache2 and remove un-needed services 5 | RUN apk update && \ 6 | apk add openrc apache2 bash ca-certificates && \ 7 | rc-update add apache2 && \ 8 | rm -rf /etc/init.d/networking /etc/init.d/hwdrivers /var/cache/apk/* /tmp/* 9 | -------------------------------------------------------------------------------- /integration-tests/goss/centos7/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | httpd: 3 | installed: true 4 | versions: 5 | - 2.4.6 6 | port: 7 | tcp6:80: 8 | listening: true 9 | ip: 10 | - '::' 11 | service: 12 | httpd: 13 | enabled: true 14 | running: true 15 | process: 16 | httpd: 17 | running: true 18 | -------------------------------------------------------------------------------- /system/service.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "strings" 4 | 5 | type Service interface { 6 | Service() string 7 | Exists() (bool, error) 8 | Enabled() (bool, error) 9 | Running() (bool, error) 10 | } 11 | 12 | func invalidService(s string) bool { 13 | if strings.ContainsRune(s, '/') { 14 | return true 15 | } 16 | return false 17 | } 18 | -------------------------------------------------------------------------------- /integration-tests/goss/wheezy/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | apache2: 3 | installed: true 4 | versions: 5 | - 2.2.22-13+deb7u7 6 | port: 7 | tcp6:80: 8 | listening: true 9 | ip: 10 | - '::' 11 | service: 12 | apache2: 13 | enabled: true 14 | running: true 15 | process: 16 | apache2: 17 | running: true 18 | -------------------------------------------------------------------------------- /integration-tests/goss/goss-service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | foobar: 4 | enabled: false 5 | running: false 6 | {{ if .Env.OS | regexMatch "centos[7]" }} 7 | httpd: 8 | {{else}} 9 | apache2: 10 | {{end}} 11 | {{ if .Env.OS | regexMatch "precise" }} 12 | enabled: false 13 | {{else}} 14 | enabled: true 15 | {{end}} 16 | running: true 17 | -------------------------------------------------------------------------------- /integration-tests/goss/precise/goss-aa-expected.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | apache2: 3 | installed: true 4 | versions: 5 | - 2.2.22-1ubuntu1.11 6 | port: 7 | tcp:80: 8 | listening: true 9 | ip: 10 | - 0.0.0.0 11 | service: 12 | apache2: 13 | enabled: false 14 | running: true 15 | process: 16 | apache2: 17 | running: true 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /main 3 | *.bak 4 | /goss 5 | /release 6 | /integration-tests/goss/goss 7 | /integration-tests/**/*-generated* 8 | /vendor/ 9 | /integration-tests/**/goss-linux-386 10 | /integration-tests/**/goss-linux-amd64 11 | 12 | # Random stuff for my local testing/development that I don't want checked in 13 | tmp/ 14 | /goss.yaml 15 | 16 | /.idea 17 | -------------------------------------------------------------------------------- /integration-tests/goss/arch/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | package: 3 | curl: 4 | installed: true 5 | pacman: 6 | installed: true 7 | foobar: 8 | installed: false 9 | user: 10 | root: 11 | exists: true 12 | uid: 0 13 | gid: 0 14 | home: "/root" 15 | file: 16 | "/foo": 17 | exists: true 18 | filetype: symlink 19 | gossfile: 20 | "../goss-shared.yaml": {} 21 | -------------------------------------------------------------------------------- /development/build_images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeu 4 | 5 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | INTEGRATION_TEST_DIR="$SCRIPT_DIR/../integration-tests/" 7 | 8 | 9 | for docker_file in $INTEGRATION_TEST_DIR/Dockerfile_*; do 10 | [[ $docker_file == *.md5 ]] && continue 11 | os=$(cut -d '_' -f2 <<<"$docker_file") 12 | docker build -t "aelsabbahy/goss_${os}:latest" - < "$docker_file" 13 | done 14 | -------------------------------------------------------------------------------- /integration-tests/goss/alpine3/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | autofs: 4 | enabled: false 5 | running: false 6 | user: 7 | apache: 8 | exists: true 9 | uid: 1000 10 | gid: 1000 11 | groups: 12 | - apache 13 | home: "/var/www" 14 | group: 15 | apache: 16 | exists: true 17 | gid: 1000 18 | process: 19 | httpd: 20 | running: true 21 | port: 22 | tcp6:80: 23 | listening: true 24 | ip: 25 | - "::" 26 | gossfile: 27 | "../goss-s*.yaml": {} 28 | -------------------------------------------------------------------------------- /integration-tests/goss/centos7/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | autofs: 4 | enabled: false 5 | running: false 6 | user: 7 | apache: 8 | exists: true 9 | uid: 48 10 | gid: 48 11 | groups: 12 | - apache 13 | home: "/usr/share/httpd" 14 | group: 15 | apache: 16 | exists: true 17 | gid: 48 18 | process: 19 | httpd: 20 | running: true 21 | port: 22 | tcp6:80: 23 | listening: true 24 | ip: 25 | - "::" 26 | gossfile: 27 | "../goss-s*.yaml": {} 28 | -------------------------------------------------------------------------------- /integration-tests/goss/precise/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | autofs: 4 | enabled: true 5 | running: true 6 | user: 7 | www-data: 8 | exists: true 9 | uid: 33 10 | gid: 33 11 | groups: 12 | - www-data 13 | home: "/var/www" 14 | group: 15 | www-data: 16 | exists: true 17 | gid: 33 18 | process: 19 | apache2: 20 | running: true 21 | port: 22 | tcp:80: 23 | listening: true 24 | ip: 25 | - 0.0.0.0 26 | gossfile: 27 | "../goss-s*.yaml": {} 28 | -------------------------------------------------------------------------------- /integration-tests/goss/wheezy/goss.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | service: 3 | autofs: 4 | enabled: false 5 | running: false 6 | user: 7 | www-data: 8 | exists: true 9 | uid: 33 10 | gid: 33 11 | groups: 12 | - www-data 13 | home: "/var/www" 14 | group: 15 | www-data: 16 | exists: true 17 | gid: 33 18 | process: 19 | apache2: 20 | running: true 21 | port: 22 | tcp6:80: 23 | listening: true 24 | ip: 25 | - "::" 26 | gossfile: 27 | "../goss-s*.yaml": {} 28 | -------------------------------------------------------------------------------- /development/push_images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeu 4 | 5 | SCRIPT_DIR=$(readlink -f "$(dirname "$0")") 6 | images=$(docker images | grep '^aelsabbahy/goss_.*latest' | awk '$0=$1') 7 | 8 | # Use md5sum to determine if CI needs to do a docker build 9 | pushd "$SCRIPT_DIR/../integration-tests"; 10 | for dockerfile in Dockerfile_*;do 11 | [[ $dockerfile == *.md5 ]] && continue 12 | md5sum "$dockerfile" > "${dockerfile}.md5" 13 | done 14 | popd 15 | 16 | for image in $images; do 17 | docker push "${image}:latest" 18 | done 19 | -------------------------------------------------------------------------------- /system/gossfile.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "github.com/aelsabbahy/goss/util" 4 | 5 | type Gossfile interface { 6 | Path() string 7 | Exists() (bool, error) 8 | } 9 | 10 | type DefGossfile struct { 11 | path string 12 | } 13 | 14 | func (g *DefGossfile) Path() string { 15 | return g.path 16 | } 17 | 18 | // Stub out 19 | func (g *DefGossfile) Exists() (bool, error) { 20 | return false, nil 21 | } 22 | 23 | func NewDefGossfile(path string, system *System, config util.Config) Gossfile { 24 | return &DefGossfile{path: path} 25 | } 26 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_precise: -------------------------------------------------------------------------------- 1 | FROM ubuntu-upstart:precise 2 | MAINTAINER Ahmed 3 | 4 | # dpkg-divert - https://github.com/docker/docker/issues/1024#issuecomment-20018600 5 | RUN mv /sbin/initctl /sbin/initctl.orig && \ 6 | ln -s /bin/true /sbin/initctl && \ 7 | apt-get update && \ 8 | apt-get install -y apache2=2.2.22-1ubuntu1.11 chkconfig vim-tiny autofs && \ 9 | apt-get remove -y vim-tiny && \ 10 | apt-get clean && \ 11 | rm -f /sbin/initctl && \ 12 | mv /sbin/initctl.orig /sbin/initctl && \ 13 | echo manual > /etc/init/apache2.override 14 | 15 | RUN chkconfig apache2 on 16 | -------------------------------------------------------------------------------- /outputs/silent.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "io" 5 | "time" 6 | 7 | "github.com/aelsabbahy/goss/resource" 8 | ) 9 | 10 | type Silent struct{} 11 | 12 | func (r Silent) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 13 | var failed int 14 | for resultGroup := range results { 15 | for _, testResult := range resultGroup { 16 | switch testResult.Result { 17 | case resource.FAIL: 18 | failed++ 19 | } 20 | } 21 | } 22 | 23 | if failed > 0 { 24 | return 1 25 | } 26 | return 0 27 | } 28 | 29 | func init() { 30 | RegisterOutputer("silent", &Silent{}) 31 | } 32 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/aelsabbahy/goss 2 | import: 3 | - package: github.com/aelsabbahy/GOnetstat 4 | - package: github.com/cheekybits/genny 5 | subpackages: 6 | - generic 7 | - package: github.com/urfave/cli 8 | - package: github.com/fatih/color 9 | - package: github.com/mitchellh/go-ps 10 | - package: github.com/oleiade/reflections 11 | - package: github.com/onsi/gomega 12 | subpackages: 13 | - types 14 | - package: github.com/opencontainers/runc 15 | subpackages: 16 | - libcontainer/user 17 | - package: gopkg.in/yaml.v2 18 | - package: github.com/achanda/go-sysctl 19 | - package: github.com/docker/docker 20 | subpackages: 21 | - pkg/mount 22 | - package: github.com/patrickmn/go-cache 23 | - package: github.com/miekg/dns 24 | -------------------------------------------------------------------------------- /resource/gossfile.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Gossfile struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Path string `json:"-" yaml:"-"` 12 | } 13 | 14 | func (g *Gossfile) ID() string { return g.Path } 15 | func (g *Gossfile) SetID(id string) { g.Path = id } 16 | 17 | func (g *Gossfile) GetTitle() string { return g.Title } 18 | func (g *Gossfile) GetMeta() meta { return g.Meta } 19 | 20 | func NewGossfile(sysGossfile system.Gossfile, config util.Config) (*Gossfile, error) { 21 | path := sysGossfile.Path() 22 | return &Gossfile{ 23 | Path: path, 24 | }, nil 25 | } 26 | -------------------------------------------------------------------------------- /integration-tests/Dockerfile_centos7: -------------------------------------------------------------------------------- 1 | FROM centos:7.2.1511 2 | MAINTAINER Ahmed 3 | 4 | ENV container docker 5 | RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \ 6 | rm -f /lib/systemd/system/multi-user.target.wants/*;\ 7 | rm -f /etc/systemd/system/*.wants/*;\ 8 | rm -f /lib/systemd/system/local-fs.target.wants/*; \ 9 | rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ 10 | rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ 11 | rm -f /lib/systemd/system/basic.target.wants/*;\ 12 | rm -f /lib/systemd/system/anaconda.target.wants/*; 13 | VOLUME [ "/sys/fs/cgroup" ] 14 | CMD ["/usr/sbin/init"] 15 | 16 | RUN yum -y --disablerepo='*' --enablerepo=base install httpd && yum clean all 17 | 18 | RUN systemctl enable httpd 19 | RUN chmod 700 ~root 20 | -------------------------------------------------------------------------------- /system/kernel_param.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/achanda/go-sysctl" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type KernelParam interface { 9 | Key() string 10 | Exists() (bool, error) 11 | Value() (string, error) 12 | } 13 | 14 | type DefKernelParam struct { 15 | key string 16 | value string 17 | } 18 | 19 | func NewDefKernelParam(key string, system *System, config util.Config) KernelParam { 20 | return &DefKernelParam{ 21 | key: key, 22 | } 23 | } 24 | 25 | func (k *DefKernelParam) ID() string { 26 | return k.key 27 | } 28 | 29 | func (k *DefKernelParam) Key() string { 30 | return k.key 31 | } 32 | 33 | func (k *DefKernelParam) Exists() (bool, error) { 34 | if _, err := k.Value(); err != nil { 35 | return false, nil 36 | } 37 | return true, nil 38 | } 39 | 40 | func (k *DefKernelParam) Value() (string, error) { 41 | return sysctl.Get(k.key) 42 | } 43 | -------------------------------------------------------------------------------- /system/group.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/util" 5 | "github.com/opencontainers/runc/libcontainer/user" 6 | ) 7 | 8 | type Group interface { 9 | Groupname() string 10 | Exists() (bool, error) 11 | GID() (int, error) 12 | } 13 | 14 | type DefGroup struct { 15 | groupname string 16 | } 17 | 18 | func NewDefGroup(groupname string, system *System, config util.Config) Group { 19 | return &DefGroup{groupname: groupname} 20 | } 21 | 22 | func (u *DefGroup) Groupname() string { 23 | return u.groupname 24 | } 25 | 26 | func (u *DefGroup) Exists() (bool, error) { 27 | _, err := user.LookupGroup(u.groupname) 28 | if err != nil { 29 | return false, nil 30 | } 31 | return true, nil 32 | } 33 | 34 | func (u *DefGroup) GID() (int, error) { 35 | group, err := user.LookupGroup(u.groupname) 36 | if err != nil { 37 | return 0, err 38 | } 39 | 40 | return group.Gid, nil 41 | } 42 | -------------------------------------------------------------------------------- /system/package.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/aelsabbahy/goss/util" 7 | ) 8 | 9 | type Package interface { 10 | Name() string 11 | Exists() (bool, error) 12 | Installed() (bool, error) 13 | Versions() ([]string, error) 14 | } 15 | 16 | var ErrNullPackage = errors.New("Could not detect Package type on this system, please use --package flag to explicity set it") 17 | 18 | type NullPackage struct { 19 | name string 20 | } 21 | 22 | func NewNullPackage(name string, system *System, config util.Config) Package { 23 | return &NullPackage{name: name} 24 | } 25 | 26 | func (p *NullPackage) Name() string { return p.name } 27 | 28 | func (p *NullPackage) Exists() (bool, error) { return p.Installed() } 29 | 30 | func (p *NullPackage) Installed() (bool, error) { 31 | return false, ErrNullPackage 32 | } 33 | 34 | func (p *NullPackage) Versions() ([]string, error) { 35 | return nil, ErrNullPackage 36 | } 37 | -------------------------------------------------------------------------------- /outputs/nagios.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/aelsabbahy/goss/resource" 9 | ) 10 | 11 | type Nagios struct{} 12 | 13 | func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 14 | var testCount, failed, skipped int 15 | for resultGroup := range results { 16 | for _, testResult := range resultGroup { 17 | switch testResult.Result { 18 | case resource.FAIL: 19 | failed++ 20 | case resource.SKIP: 21 | skipped++ 22 | } 23 | testCount++ 24 | } 25 | } 26 | 27 | duration := time.Since(startTime) 28 | if failed > 0 { 29 | fmt.Fprintf(w, "GOSS CRITICAL - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs\n", testCount, failed, skipped, duration.Seconds()) 30 | return 2 31 | } 32 | fmt.Fprintf(w, "GOSS OK - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs\n", testCount, failed, skipped, duration.Seconds()) 33 | return 0 34 | } 35 | 36 | func init() { 37 | RegisterOutputer("nagios", &Nagios{}) 38 | } 39 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | { 4 | set -e 5 | 6 | LATEST="v0.3.3" 7 | DGOSS_VER=$GOSS_VER 8 | 9 | if [ -z "$GOSS_VER" ]; then 10 | GOSS_VER=${GOSS_VER:-$LATEST} 11 | DGOSS_VER='master' 12 | fi 13 | GOSS_DST=${GOSS_DST:-/usr/local/bin} 14 | INSTALL_LOC="${GOSS_DST%/}/goss" 15 | DGOSS_INSTALL_LOC="${GOSS_DST%/}/dgoss" 16 | touch "$INSTALL_LOC" || { echo "ERROR: Cannot write to $GOSS_DST set GOSS_DST elsewhere or use sudo"; exit 1; } 17 | 18 | arch="" 19 | if [ "$(uname -m)" = "x86_64" ]; then 20 | arch="amd64" 21 | else 22 | arch="386" 23 | fi 24 | 25 | url="https://github.com/aelsabbahy/goss/releases/download/$GOSS_VER/goss-linux-$arch" 26 | 27 | echo "Downloading $url" 28 | curl -L "$url" -o "$INSTALL_LOC" 29 | chmod +rx "$INSTALL_LOC" 30 | echo "Goss $GOSS_VER has been installed to $INSTALL_LOC" 31 | echo "goss --version" 32 | "$INSTALL_LOC" --version 33 | 34 | dgoss_url="https://raw.githubusercontent.com/aelsabbahy/goss/$DGOSS_VER/extras/dgoss/dgoss" 35 | echo "Downloading $dgoss_url" 36 | curl -L "$dgoss_url" -o "$DGOSS_INSTALL_LOC" 37 | chmod +rx "$DGOSS_INSTALL_LOC" 38 | echo "dgoss $DGOSS_VER has been installed to $DGOSS_INSTALL_LOC" 39 | } 40 | -------------------------------------------------------------------------------- /util/command.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | //"fmt" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | type Command struct { 11 | name string 12 | Cmd *exec.Cmd 13 | Stdout, Stderr bytes.Buffer 14 | Err error 15 | Status int 16 | } 17 | 18 | func NewCommand(name string, arg ...string) *Command { 19 | //fmt.Println(arg) 20 | command := new(Command) 21 | command.name = name 22 | command.Cmd = exec.Command(name, arg...) 23 | return command 24 | } 25 | 26 | func (c *Command) Run() error { 27 | c.Cmd.Stdout = &c.Stdout 28 | c.Cmd.Stderr = &c.Stderr 29 | 30 | if _, err := exec.LookPath(c.name); err != nil { 31 | c.Err = err 32 | return c.Err 33 | } 34 | 35 | if err := c.Cmd.Start(); err != nil { 36 | c.Err = err 37 | //log.Fatalf("Cmd.Start: %v") 38 | } 39 | 40 | if err := c.Cmd.Wait(); err != nil { 41 | c.Err = err 42 | if exiterr, ok := err.(*exec.ExitError); ok { 43 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 44 | c.Status = status.ExitStatus() 45 | //log.Printf("Exit Status: %d", status.ExitStatus()) 46 | } 47 | } 48 | } else { 49 | c.Status = 0 50 | } 51 | return c.Err 52 | } 53 | -------------------------------------------------------------------------------- /resource/process.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Process struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Executable string `json:"-" yaml:"-"` 12 | Running matcher `json:"running" yaml:"running"` 13 | } 14 | 15 | func (p *Process) ID() string { return p.Executable } 16 | func (p *Process) SetID(id string) { p.Executable = id } 17 | 18 | func (p *Process) GetTitle() string { return p.Title } 19 | func (p *Process) GetMeta() meta { return p.Meta } 20 | 21 | func (p *Process) Validate(sys *system.System) []TestResult { 22 | skip := false 23 | sysProcess := sys.NewProcess(p.Executable, sys, util.Config{}) 24 | 25 | var results []TestResult 26 | results = append(results, ValidateValue(p, "running", p.Running, sysProcess.Running, skip)) 27 | return results 28 | } 29 | 30 | func NewProcess(sysProcess system.Process, config util.Config) (*Process, error) { 31 | executable := sysProcess.Executable() 32 | running, _ := sysProcess.Running() 33 | return &Process{ 34 | Executable: executable, 35 | Running: running, 36 | }, nil 37 | } 38 | -------------------------------------------------------------------------------- /system/package_rpm.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type RpmPackage struct { 11 | name string 12 | versions []string 13 | loaded bool 14 | installed bool 15 | } 16 | 17 | func NewRpmPackage(name string, system *System, config util.Config) Package { 18 | return &RpmPackage{name: name} 19 | } 20 | 21 | func (p *RpmPackage) setup() { 22 | if p.loaded { 23 | return 24 | } 25 | p.loaded = true 26 | cmd := util.NewCommand("rpm", "-q", "--nosignature", "--nohdrchk", "--nodigest", "--qf", "%{VERSION}\n", p.name) 27 | if err := cmd.Run(); err != nil { 28 | return 29 | } 30 | p.installed = true 31 | p.versions = strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") 32 | } 33 | 34 | func (p *RpmPackage) Name() string { 35 | return p.name 36 | } 37 | 38 | func (p *RpmPackage) Exists() (bool, error) { return p.Installed() } 39 | 40 | func (p *RpmPackage) Installed() (bool, error) { 41 | p.setup() 42 | 43 | return p.installed, nil 44 | } 45 | 46 | func (p *RpmPackage) Versions() ([]string, error) { 47 | p.setup() 48 | if len(p.versions) == 0 { 49 | return p.versions, errors.New("Package version not found") 50 | } 51 | return p.versions, nil 52 | } 53 | -------------------------------------------------------------------------------- /resource/kernel_param.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type KernelParam struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Key string `json:"-" yaml:"-"` 12 | Value matcher `json:"value" yaml:"value"` 13 | } 14 | 15 | func (a *KernelParam) ID() string { return a.Key } 16 | func (a *KernelParam) SetID(id string) { a.Key = id } 17 | 18 | // FIXME: Can this be refactored? 19 | func (r *KernelParam) GetTitle() string { return r.Title } 20 | func (r *KernelParam) GetMeta() meta { return r.Meta } 21 | 22 | func (a *KernelParam) Validate(sys *system.System) []TestResult { 23 | skip := false 24 | sysKernelParam := sys.NewKernelParam(a.Key, sys, util.Config{}) 25 | 26 | var results []TestResult 27 | results = append(results, ValidateValue(a, "value", a.Value, sysKernelParam.Value, skip)) 28 | return results 29 | } 30 | 31 | func NewKernelParam(sysKernelParam system.KernelParam, config util.Config) (*KernelParam, error) { 32 | key := sysKernelParam.Key() 33 | value, err := sysKernelParam.Value() 34 | a := &KernelParam{ 35 | Key: key, 36 | Value: value, 37 | } 38 | return a, err 39 | } 40 | -------------------------------------------------------------------------------- /outputs/nagios_verbose.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/aelsabbahy/goss/resource" 10 | ) 11 | 12 | type NagiosVerbose struct{} 13 | 14 | func (r NagiosVerbose) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 15 | var testCount, failed, skipped int 16 | 17 | var summary map[int]string 18 | summary = make(map[int]string) 19 | 20 | for resultGroup := range results { 21 | for _, testResult := range resultGroup { 22 | switch testResult.Result { 23 | case resource.FAIL: 24 | summary[failed] = "Fail " + strconv.Itoa(failed+1) + " - " + humanizeResult2(testResult) + "\n" 25 | failed++ 26 | case resource.SKIP: 27 | skipped++ 28 | } 29 | testCount++ 30 | } 31 | } 32 | 33 | duration := time.Since(startTime) 34 | if failed > 0 { 35 | fmt.Fprintf(w, "GOSS CRITICAL - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs\n", testCount, failed, skipped, duration.Seconds()) 36 | for i := 0; i < failed; i++ { 37 | fmt.Fprintf(w, "%s", summary[i]) 38 | } 39 | return 2 40 | } 41 | fmt.Fprintf(w, "GOSS OK - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs\n", testCount, failed, skipped, duration.Seconds()) 42 | return 0 43 | } 44 | 45 | func init() { 46 | RegisterOutputer("nagios_verbose", &NagiosVerbose{}) 47 | } 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | 6 | sudo: required 7 | dist: trusty 8 | 9 | services: 10 | - docker 11 | 12 | before_install: 13 | - curl -L https://github.com/Masterminds/glide/releases/download/0.10.2/glide-0.10.2-linux-amd64.zip 14 | > glide.zip 15 | - unzip glide.zip 16 | - export PATH="$PATH:$PWD/linux-amd64" 17 | - go get -u github.com/golang/lint/golint 18 | 19 | script: 20 | - make deps 21 | - make 22 | 23 | deploy: 24 | provider: releases 25 | api_key: 26 | secure: hEHCC4EN7iHz7pIWKRn2qw22NTqUxnuBp59wfAlLBtV26j5rHMzSu8mlxkJInusDUGLJiNLrZPRWN0mzOdIXalbUeLhlX7EflJgEj6Q0MchUR69LzCAp0KMIFL1Sfq0v81VgujRLUUy5utxDL8Er4tZknn2PpXAMzpO+ozjNRDhhSEM4iMXfY3bcOIMnx6XRgCjFCb036wlBgOfdgv5fwm2PP638DTKar4W6ZZbqCQByhJ5RyL3BMDPTT0moA/tYbG+FA6p6Rme1OcBkMnpsiJZoB3u8gxsNiEJ43/C2RcULW/18qqp2UVD5FipSDYP7GQ5ugKCbgpWXb0Ctl8o4hv1UsNl0XoyJhAt0PRp6vqnyy6LWB2FzX30Xj/vGIhO/IfiJvspHxpatTk7Esjr46K4u9ao/x63LX6F6yI1ZTfbzt2MhRYRjwh4ORNfqhysuzXChftX1S9hj6s6gO0/zqoOsRK/PK8DProbUn4bxrGOBzi16P0GEk4agWWUm74Pis9qCThXNW8MXEV936KvE1wb1RxTACYvFBtO2IM5eQ26t2Y7mGJd7FJup9LR4oUtUTSbYo5P2Sal6xntBKH5P4nwEtM+TtHoeSCKQ3X5i1VSdvAH7soEAly6rP5d5wwPhqqx9mgUPYO/3ulvxLJOYHamrbj6nlHDXnCEoj1ZMxX4= 27 | file: 28 | - release/goss-linux-amd64 29 | - release/goss-linux-386 30 | - extras/dgoss/dgoss 31 | skip_cleanup: true 32 | on: 33 | repo: aelsabbahy/goss 34 | tags: true 35 | -------------------------------------------------------------------------------- /outputs/tap.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/aelsabbahy/goss/resource" 10 | ) 11 | 12 | type Tap struct{} 13 | 14 | func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 15 | testCount := 0 16 | failed := 0 17 | 18 | var summary map[int]string 19 | summary = make(map[int]string) 20 | 21 | for resultGroup := range results { 22 | for _, testResult := range resultGroup { 23 | switch testResult.Result { 24 | case resource.SUCCESS: 25 | summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult2(testResult) + "\n" 26 | case resource.FAIL: 27 | summary[testCount] = "not ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult2(testResult) + "\n" 28 | failed++ 29 | case resource.SKIP: 30 | summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - # SKIP " + humanizeResult2(testResult) + "\n" 31 | default: 32 | panic(fmt.Sprintf("Unexpected Result Code: %v\n", testResult.Result)) 33 | } 34 | testCount++ 35 | } 36 | } 37 | 38 | fmt.Fprintf(w, "1..%d\n", testCount) 39 | 40 | for i := 0; i < testCount; i++ { 41 | fmt.Fprintf(w, "%s", summary[i]) 42 | } 43 | 44 | if failed > 0 { 45 | return 1 46 | } 47 | 48 | return 0 49 | } 50 | 51 | func init() { 52 | RegisterOutputer("tap", &Tap{}) 53 | } 54 | -------------------------------------------------------------------------------- /util/config.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/oleiade/reflections" 9 | ) 10 | 11 | type Config struct { 12 | IgnoreList []string 13 | Timeout int 14 | AllowInsecure bool 15 | NoFollowRedirects bool 16 | Server string 17 | } 18 | 19 | type format string 20 | 21 | const ( 22 | JSON format = "json" 23 | YAML format = "yaml" 24 | ) 25 | 26 | func ValidateSections(unmarshal func(interface{}) error, i interface{}, whitelist map[string]bool) error { 27 | // Get generic input 28 | var toValidate map[string]map[string]interface{} 29 | if err := unmarshal(&toValidate); err != nil { 30 | return err 31 | } 32 | 33 | // Run input through whitelist 34 | typ := reflect.TypeOf(i) 35 | typs := strings.Split(typ.String(), ".")[1] 36 | for id, v := range toValidate { 37 | for k, _ := range v { 38 | if !whitelist[k] { 39 | return fmt.Errorf("Invalid Attribute for %s:%s: %s", typs, id, k) 40 | } 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func WhitelistAttrs(i interface{}, format format) (map[string]bool, error) { 48 | validAttrs := make(map[string]bool) 49 | tags, err := reflections.Tags(i, string(format)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | for _, v := range tags { 54 | validAttrs[strings.Split(v, ",")[0]] = true 55 | } 56 | return validAttrs, nil 57 | } 58 | -------------------------------------------------------------------------------- /outputs/rspecish.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/aelsabbahy/goss/resource" 9 | ) 10 | 11 | type Rspecish struct{} 12 | 13 | func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 14 | testCount := 0 15 | var failedOrSkipped [][]resource.TestResult 16 | var skipped, failed int 17 | for resultGroup := range results { 18 | failedOrSkippedGroup := []resource.TestResult{} 19 | for _, testResult := range resultGroup { 20 | switch testResult.Result { 21 | case resource.SUCCESS: 22 | fmt.Fprintf(w, green(".")) 23 | case resource.SKIP: 24 | fmt.Fprintf(w, yellow("S")) 25 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 26 | skipped++ 27 | case resource.FAIL: 28 | fmt.Fprintf(w, red("F")) 29 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 30 | failed++ 31 | } 32 | testCount++ 33 | } 34 | if len(failedOrSkippedGroup) > 0 { 35 | failedOrSkipped = append(failedOrSkipped, failedOrSkippedGroup) 36 | } 37 | } 38 | 39 | fmt.Fprint(w, "\n\n") 40 | fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped)) 41 | 42 | fmt.Fprint(w, summary(startTime, testCount, failed, skipped)) 43 | if failed > 0 { 44 | return 1 45 | } 46 | return 0 47 | } 48 | 49 | func init() { 50 | RegisterOutputer("rspecish", &Rspecish{}) 51 | } 52 | -------------------------------------------------------------------------------- /system/package_pacman.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type PacmanPackage struct { 11 | name string 12 | versions []string 13 | loaded bool 14 | installed bool 15 | } 16 | 17 | func NewPacmanPackage(name string, system *System, config util.Config) Package { 18 | return &PacmanPackage{name: name} 19 | } 20 | 21 | func (p *PacmanPackage) setup() { 22 | if p.loaded { 23 | return 24 | } 25 | p.loaded = true 26 | // TODO: extract versions 27 | cmd := util.NewCommand("pacman", "-Q", "--color", "never", "--noconfirm", p.name) 28 | if err := cmd.Run(); err != nil { 29 | return 30 | } 31 | p.installed = true 32 | // the output format is "pkgname version\n", so if we split the string on 33 | // whitespace, the version is the second item. 34 | p.versions = []string{strings.Fields(cmd.Stdout.String())[1]} 35 | } 36 | 37 | func (p *PacmanPackage) Name() string { 38 | return p.name 39 | } 40 | 41 | func (p *PacmanPackage) Exists() (bool, error) { return p.Installed() } 42 | 43 | func (p *PacmanPackage) Installed() (bool, error) { 44 | p.setup() 45 | 46 | return p.installed, nil 47 | } 48 | 49 | func (p *PacmanPackage) Versions() ([]string, error) { 50 | p.setup() 51 | if len(p.versions) == 0 { 52 | return p.versions, errors.New("Package version not found") 53 | } 54 | return p.versions, nil 55 | } 56 | -------------------------------------------------------------------------------- /resource/addr.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Addr struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Address string `json:"-" yaml:"-"` 12 | Reachable matcher `json:"reachable" yaml:"reachable"` 13 | Timeout int `json:"timeout" yaml:"timeout"` 14 | } 15 | 16 | func (a *Addr) ID() string { return a.Address } 17 | func (a *Addr) SetID(id string) { a.Address = id } 18 | 19 | // FIXME: Can this be refactored? 20 | func (r *Addr) GetTitle() string { return r.Title } 21 | func (r *Addr) GetMeta() meta { return r.Meta } 22 | 23 | func (a *Addr) Validate(sys *system.System) []TestResult { 24 | skip := false 25 | if a.Timeout == 0 { 26 | a.Timeout = 500 27 | } 28 | sysAddr := sys.NewAddr(a.Address, sys, util.Config{Timeout: a.Timeout}) 29 | 30 | var results []TestResult 31 | results = append(results, ValidateValue(a, "reachable", a.Reachable, sysAddr.Reachable, skip)) 32 | return results 33 | } 34 | 35 | func NewAddr(sysAddr system.Addr, config util.Config) (*Addr, error) { 36 | address := sysAddr.Address() 37 | reachable, err := sysAddr.Reachable() 38 | a := &Addr{ 39 | Address: address, 40 | Reachable: reachable, 41 | Timeout: config.Timeout, 42 | } 43 | return a, err 44 | } 45 | -------------------------------------------------------------------------------- /system/system_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "testing" 7 | ) 8 | 9 | type noInputs func() string 10 | 11 | // test that a function with no inputs returns one of the expected strings 12 | func testOutputs(f noInputs, validOutputs []string, t *testing.T) { 13 | output := f() 14 | // use reflect to get the name of the function 15 | name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 16 | failed := true 17 | for _, valid := range validOutputs { 18 | if output == valid { 19 | failed = false 20 | } 21 | } 22 | if failed { 23 | t.Errorf("Function %v returned %v, which is not one of %v", name, output, validOutputs) 24 | } 25 | } 26 | 27 | func TestPackageManager(t *testing.T) { 28 | t.Parallel() 29 | testOutputs( 30 | DetectPackageManager, 31 | []string{"deb", "rpm", "apk", "pacman", ""}, 32 | t, 33 | ) 34 | } 35 | 36 | func TestDetectService(t *testing.T) { 37 | t.Parallel() 38 | testOutputs( 39 | DetectService, 40 | []string{"systemd", "init", "alpineinit", "upstart", ""}, 41 | t, 42 | ) 43 | } 44 | 45 | func TestDetectDistro(t *testing.T) { 46 | t.Parallel() 47 | testOutputs( 48 | DetectDistro, 49 | []string{"ubuntu", "redhat", "alpine", "arch", "debian", ""}, 50 | t, 51 | ) 52 | } 53 | 54 | func TestHasCommand(t *testing.T) { 55 | t.Parallel() 56 | if !HasCommand("sh") { 57 | t.Error("System didn't have sh!") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /system/addr.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "time" 7 | 8 | "github.com/aelsabbahy/goss/util" 9 | ) 10 | 11 | type Addr interface { 12 | Address() string 13 | Exists() (bool, error) 14 | Reachable() (bool, error) 15 | } 16 | 17 | type DefAddr struct { 18 | address string 19 | Timeout int 20 | } 21 | 22 | func NewDefAddr(address string, system *System, config util.Config) Addr { 23 | addr := normalizeAddress(address) 24 | return &DefAddr{ 25 | address: addr, 26 | Timeout: config.Timeout, 27 | } 28 | } 29 | 30 | func (a *DefAddr) ID() string { 31 | return a.address 32 | } 33 | func (a *DefAddr) Address() string { 34 | return a.address 35 | } 36 | func (a *DefAddr) Exists() (bool, error) { return a.Reachable() } 37 | 38 | func (a *DefAddr) Reachable() (bool, error) { 39 | network, address := splitAddress(a.address) 40 | 41 | conn, err := net.DialTimeout(network, address, time.Duration(a.Timeout)*time.Millisecond) 42 | if err != nil { 43 | return false, nil 44 | } 45 | conn.Close() 46 | return true, nil 47 | } 48 | 49 | func splitAddress(fulladdress string) (network, address string) { 50 | split := strings.SplitN(fulladdress, "://", 2) 51 | if len(split) == 2 { 52 | return split[0], split[1] 53 | } 54 | return "tcp", fulladdress 55 | 56 | } 57 | 58 | func normalizeAddress(fulladdress string) string { 59 | net, addr := splitAddress(fulladdress) 60 | return net + "://" + addr 61 | } 62 | -------------------------------------------------------------------------------- /system/process.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | "github.com/mitchellh/go-ps" 9 | ) 10 | 11 | type Process interface { 12 | Executable() string 13 | Exists() (bool, error) 14 | Running() (bool, error) 15 | Pids() ([]int, error) 16 | } 17 | 18 | type DefProcess struct { 19 | executable string 20 | procMap map[string][]ps.Process 21 | } 22 | 23 | func NewDefProcess(executable string, system *System, config util.Config) Process { 24 | return &DefProcess{ 25 | executable: executable, 26 | procMap: system.ProcMap(), 27 | } 28 | } 29 | 30 | func (p *DefProcess) Executable() string { 31 | return p.executable 32 | } 33 | 34 | func (p *DefProcess) Exists() (bool, error) { return p.Running() } 35 | 36 | func (p *DefProcess) Pids() ([]int, error) { 37 | var pids []int 38 | for _, proc := range p.procMap[p.executable] { 39 | pids = append(pids, proc.Pid()) 40 | } 41 | return pids, nil 42 | } 43 | 44 | func (p *DefProcess) Running() (bool, error) { 45 | if _, ok := p.procMap[p.executable]; ok { 46 | return true, nil 47 | } 48 | return false, nil 49 | } 50 | 51 | func GetProcs() map[string][]ps.Process { 52 | pmap := make(map[string][]ps.Process) 53 | processes, err := ps.Processes() 54 | if err != nil { 55 | fmt.Println(err) 56 | os.Exit(1) 57 | } 58 | for _, p := range processes { 59 | pmap[p.Executable()] = append(pmap[p.Executable()], p) 60 | } 61 | 62 | return pmap 63 | } 64 | -------------------------------------------------------------------------------- /system/package_alpine.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type AlpinePackage struct { 11 | name string 12 | versions []string 13 | loaded bool 14 | installed bool 15 | } 16 | 17 | func NewAlpinePackage(name string, system *System, config util.Config) Package { 18 | return &AlpinePackage{name: name} 19 | } 20 | 21 | func (p *AlpinePackage) setup() { 22 | if p.loaded { 23 | return 24 | } 25 | p.loaded = true 26 | cmd := util.NewCommand("apk", "version", p.name) 27 | if err := cmd.Run(); err != nil { 28 | return 29 | } 30 | for _, l := range strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") { 31 | if strings.HasPrefix(l, "Installed:") || strings.HasPrefix(l, "WARNING") { 32 | continue 33 | } 34 | ver := strings.TrimPrefix(strings.Fields(l)[0], p.name+"-") 35 | p.versions = append(p.versions, ver) 36 | } 37 | 38 | if len(p.versions) > 0 { 39 | p.installed = true 40 | } 41 | } 42 | 43 | func (p *AlpinePackage) Name() string { 44 | return p.name 45 | } 46 | 47 | func (p *AlpinePackage) Exists() (bool, error) { return p.Installed() } 48 | 49 | func (p *AlpinePackage) Installed() (bool, error) { 50 | p.setup() 51 | 52 | return p.installed, nil 53 | } 54 | 55 | func (p *AlpinePackage) Versions() ([]string, error) { 56 | p.setup() 57 | if len(p.versions) == 0 { 58 | return p.versions, errors.New("Package version not found") 59 | } 60 | return p.versions, nil 61 | } 62 | -------------------------------------------------------------------------------- /system/package_deb.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type DebPackage struct { 11 | name string 12 | versions []string 13 | loaded bool 14 | installed bool 15 | } 16 | 17 | func NewDebPackage(name string, system *System, config util.Config) Package { 18 | return &DebPackage{name: name} 19 | } 20 | 21 | func (p *DebPackage) setup() { 22 | if p.loaded { 23 | return 24 | } 25 | p.loaded = true 26 | cmd := util.NewCommand("dpkg-query", "-f", "${Status} ${Version}\n", "-W", p.name) 27 | if err := cmd.Run(); err != nil { 28 | return 29 | } 30 | for _, l := range strings.Split(strings.TrimSpace(cmd.Stdout.String()), "\n") { 31 | if !(strings.HasPrefix(l, "install ok installed") || strings.HasPrefix(l, "hold ok installed")) { 32 | continue 33 | } 34 | ver := strings.Fields(l)[3] 35 | p.versions = append(p.versions, ver) 36 | } 37 | 38 | if len(p.versions) > 0 { 39 | p.installed = true 40 | } 41 | } 42 | 43 | func (p *DebPackage) Name() string { 44 | return p.name 45 | } 46 | 47 | func (p *DebPackage) Exists() (bool, error) { return p.Installed() } 48 | 49 | func (p *DebPackage) Installed() (bool, error) { 50 | p.setup() 51 | 52 | return p.installed, nil 53 | } 54 | 55 | func (p *DebPackage) Versions() ([]string, error) { 56 | p.setup() 57 | if len(p.versions) == 0 { 58 | return p.versions, errors.New("Package version not found") 59 | } 60 | return p.versions, nil 61 | } 62 | -------------------------------------------------------------------------------- /resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/aelsabbahy/goss/system" 10 | "github.com/oleiade/reflections" 11 | ) 12 | 13 | type Resource interface { 14 | Validate(*system.System) []TestResult 15 | SetID(string) 16 | } 17 | 18 | type ResourceRead interface { 19 | ID() string 20 | GetTitle() string 21 | GetMeta() meta 22 | } 23 | 24 | type matcher interface{} 25 | type meta map[string]interface{} 26 | 27 | func contains(a []string, s string) bool { 28 | for _, e := range a { 29 | if m, _ := filepath.Match(e, s); m { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | func deprecateAtoI(depr interface{}, desc string) interface{} { 37 | s, ok := depr.(string) 38 | if !ok { 39 | return depr 40 | } 41 | fmt.Printf("DEPRECATION WARNING: %s should be an integer not a string\n", desc) 42 | i, err := strconv.Atoi(s) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return float64(i) 47 | } 48 | 49 | func validAttrs(i interface{}, t string) (map[string]bool, error) { 50 | validAttrs := make(map[string]bool) 51 | tags, err := reflections.Tags(i, t) 52 | if err != nil { 53 | return nil, err 54 | } 55 | for _, v := range tags { 56 | validAttrs[strings.Split(v, ",")[0]] = true 57 | } 58 | return validAttrs, nil 59 | } 60 | 61 | func shouldSkip(results []TestResult) bool { 62 | if results[0].Err != nil || results[0].Found[0] == "false" { 63 | return true 64 | } 65 | return false 66 | } 67 | -------------------------------------------------------------------------------- /system/interface.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/aelsabbahy/goss/util" 7 | ) 8 | 9 | type Interface interface { 10 | Name() string 11 | Exists() (bool, error) 12 | Addrs() ([]string, error) 13 | } 14 | 15 | type DefInterface struct { 16 | name string 17 | loaded bool 18 | exists bool 19 | iface *net.Interface 20 | err error 21 | } 22 | 23 | func NewDefInterface(name string, systei *System, config util.Config) Interface { 24 | return &DefInterface{ 25 | name: name, 26 | } 27 | } 28 | 29 | func (i *DefInterface) setup() error { 30 | if i.loaded { 31 | return i.err 32 | } 33 | i.loaded = true 34 | 35 | iface, err := net.InterfaceByName(i.name) 36 | if err != nil { 37 | i.exists = false 38 | i.err = err 39 | return i.err 40 | } 41 | i.iface = iface 42 | i.exists = true 43 | return nil 44 | } 45 | 46 | func (i *DefInterface) ID() string { 47 | return i.name 48 | } 49 | 50 | func (i *DefInterface) Name() string { 51 | return i.name 52 | } 53 | 54 | func (i *DefInterface) Exists() (bool, error) { 55 | if err := i.setup(); err != nil { 56 | return false, nil 57 | } 58 | 59 | return i.exists, nil 60 | } 61 | 62 | func (i *DefInterface) Addrs() ([]string, error) { 63 | if err := i.setup(); err != nil { 64 | return nil, err 65 | } 66 | 67 | addrs, err := i.iface.Addrs() 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | var ret []string 73 | for _, addr := range addrs { 74 | ret = append(ret, addr.String()) 75 | } 76 | return ret, nil 77 | } 78 | -------------------------------------------------------------------------------- /resource/service.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Service struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Service string `json:"-" yaml:"-"` 12 | Enabled matcher `json:"enabled" yaml:"enabled"` 13 | Running matcher `json:"running" yaml:"running"` 14 | } 15 | 16 | func (s *Service) ID() string { return s.Service } 17 | func (s *Service) SetID(id string) { s.Service = id } 18 | 19 | func (s *Service) GetTitle() string { return s.Title } 20 | func (s *Service) GetMeta() meta { return s.Meta } 21 | 22 | func (s *Service) Validate(sys *system.System) []TestResult { 23 | skip := false 24 | sysservice := sys.NewService(s.Service, sys, util.Config{}) 25 | 26 | var results []TestResult 27 | results = append(results, ValidateValue(s, "enabled", s.Enabled, sysservice.Enabled, skip)) 28 | results = append(results, ValidateValue(s, "running", s.Running, sysservice.Running, skip)) 29 | return results 30 | } 31 | 32 | func NewService(sysService system.Service, config util.Config) (*Service, error) { 33 | service := sysService.Service() 34 | enabled, err := sysService.Enabled() 35 | if err != nil { 36 | return nil, err 37 | } 38 | running, err := sysService.Running() 39 | if err != nil { 40 | return nil, err 41 | } 42 | return &Service{ 43 | Service: service, 44 | Enabled: enabled, 45 | Running: running, 46 | }, nil 47 | } 48 | -------------------------------------------------------------------------------- /resource/port.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Port struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Port string `json:"-" yaml:"-"` 12 | Listening matcher `json:"listening" yaml:"listening"` 13 | IP matcher `json:"ip,omitempty" yaml:"ip,omitempty"` 14 | } 15 | 16 | func (p *Port) ID() string { return p.Port } 17 | func (p *Port) SetID(id string) { p.Port = id } 18 | 19 | func (p *Port) GetTitle() string { return p.Title } 20 | func (p *Port) GetMeta() meta { return p.Meta } 21 | 22 | func (p *Port) Validate(sys *system.System) []TestResult { 23 | skip := false 24 | sysPort := sys.NewPort(p.Port, sys, util.Config{}) 25 | 26 | var results []TestResult 27 | results = append(results, ValidateValue(p, "listening", p.Listening, sysPort.Listening, skip)) 28 | if shouldSkip(results) { 29 | skip = true 30 | } 31 | if p.IP != nil { 32 | results = append(results, ValidateValue(p, "ip", p.IP, sysPort.IP, skip)) 33 | } 34 | return results 35 | } 36 | 37 | func NewPort(sysPort system.Port, config util.Config) (*Port, error) { 38 | port := sysPort.Port() 39 | listening, _ := sysPort.Listening() 40 | p := &Port{ 41 | Port: port, 42 | Listening: listening, 43 | } 44 | if !contains(config.IgnoreList, "ip") { 45 | if ip, err := sysPort.IP(); err == nil { 46 | p.IP = ip 47 | } 48 | } 49 | return p, nil 50 | } 51 | -------------------------------------------------------------------------------- /outputs/documentation.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/aelsabbahy/goss/resource" 9 | ) 10 | 11 | type Documentation struct{} 12 | 13 | func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 14 | testCount := 0 15 | var failedOrSkipped [][]resource.TestResult 16 | var skipped, failed int 17 | for resultGroup := range results { 18 | failedOrSkippedGroup := []resource.TestResult{} 19 | first := resultGroup[0] 20 | header := header(first) 21 | if header != "" { 22 | fmt.Fprint(w, header) 23 | } 24 | for _, testResult := range resultGroup { 25 | switch testResult.Result { 26 | case resource.SUCCESS: 27 | fmt.Fprintln(w, humanizeResult(testResult)) 28 | case resource.SKIP: 29 | fmt.Fprintln(w, humanizeResult(testResult)) 30 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 31 | skipped++ 32 | case resource.FAIL: 33 | fmt.Fprintln(w, humanizeResult(testResult)) 34 | failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) 35 | failed++ 36 | } 37 | testCount++ 38 | } 39 | if len(failedOrSkippedGroup) > 0 { 40 | failedOrSkipped = append(failedOrSkipped, failedOrSkippedGroup) 41 | } 42 | } 43 | 44 | fmt.Fprint(w, "\n\n") 45 | fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped)) 46 | 47 | fmt.Fprint(w, summary(startTime, testCount, failed, skipped)) 48 | if failed > 0 { 49 | return 1 50 | } 51 | return 0 52 | } 53 | 54 | func init() { 55 | RegisterOutputer("documentation", &Documentation{}) 56 | } 57 | -------------------------------------------------------------------------------- /resource/package.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Package struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Name string `json:"-" yaml:"-"` 12 | Installed matcher `json:"installed" yaml:"installed"` 13 | Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"` 14 | } 15 | 16 | func (p *Package) ID() string { return p.Name } 17 | func (p *Package) SetID(id string) { p.Name = id } 18 | 19 | func (p *Package) GetTitle() string { return p.Title } 20 | func (p *Package) GetMeta() meta { return p.Meta } 21 | 22 | func (p *Package) Validate(sys *system.System) []TestResult { 23 | skip := false 24 | sysPkg := sys.NewPackage(p.Name, sys, util.Config{}) 25 | 26 | var results []TestResult 27 | results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip)) 28 | if shouldSkip(results) { 29 | skip = true 30 | } 31 | if p.Versions != nil { 32 | results = append(results, ValidateValue(p, "version", p.Versions, sysPkg.Versions, skip)) 33 | } 34 | return results 35 | } 36 | 37 | func NewPackage(sysPackage system.Package, config util.Config) (*Package, error) { 38 | name := sysPackage.Name() 39 | installed, _ := sysPackage.Installed() 40 | p := &Package{ 41 | Name: name, 42 | Installed: installed, 43 | } 44 | if !contains(config.IgnoreList, "versions") { 45 | if versions, err := sysPackage.Versions(); err == nil { 46 | p.Versions = versions 47 | } 48 | } 49 | return p, nil 50 | } 51 | -------------------------------------------------------------------------------- /resource/interface.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Interface struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Name string `json:"-" yaml:"-"` 12 | Exists matcher `json:"exists" yaml:"exists"` 13 | Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"` 14 | } 15 | 16 | func (i *Interface) ID() string { return i.Name } 17 | func (i *Interface) SetID(id string) { i.Name = id } 18 | 19 | // FIXME: Can this be refactored? 20 | func (i *Interface) GetTitle() string { return i.Title } 21 | func (i *Interface) GetMeta() meta { return i.Meta } 22 | 23 | func (i *Interface) Validate(sys *system.System) []TestResult { 24 | skip := false 25 | sysInterface := sys.NewInterface(i.Name, sys, util.Config{}) 26 | 27 | var results []TestResult 28 | results = append(results, ValidateValue(i, "exists", i.Exists, sysInterface.Exists, skip)) 29 | if shouldSkip(results) { 30 | skip = true 31 | } 32 | if i.Addrs != nil { 33 | results = append(results, ValidateValue(i, "addrs", i.Addrs, sysInterface.Addrs, skip)) 34 | } 35 | return results 36 | } 37 | 38 | func NewInterface(sysInterface system.Interface, config util.Config) (*Interface, error) { 39 | name := sysInterface.Name() 40 | exists, _ := sysInterface.Exists() 41 | i := &Interface{ 42 | Name: name, 43 | Exists: exists, 44 | } 45 | if !contains(config.IgnoreList, "addrs") { 46 | if addrs, err := sysInterface.Addrs(); err == nil { 47 | i.Addrs = addrs 48 | } 49 | } 50 | return i, nil 51 | } 52 | -------------------------------------------------------------------------------- /outputs/json.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "github.com/aelsabbahy/goss/resource" 10 | "github.com/fatih/color" 11 | ) 12 | 13 | type Json struct{} 14 | 15 | func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 16 | color.NoColor = true 17 | testCount := 0 18 | failed := 0 19 | var resultsOut []map[string]interface{} 20 | for resultGroup := range results { 21 | for _, testResult := range resultGroup { 22 | if !testResult.Successful { 23 | failed++ 24 | } 25 | m := struct2map(testResult) 26 | m["summary-line"] = humanizeResult(testResult) 27 | m["duration"] = int64(m["duration"].(float64)) 28 | resultsOut = append(resultsOut, m) 29 | testCount++ 30 | } 31 | } 32 | 33 | summary := make(map[string]interface{}) 34 | duration := time.Since(startTime) 35 | summary["test-count"] = testCount 36 | summary["failed-count"] = failed 37 | summary["total-duration"] = duration 38 | summary["summary-line"] = fmt.Sprintf("Count: %d, Failed: %d, Duration: %.3fs", testCount, failed, duration.Seconds()) 39 | 40 | out := make(map[string]interface{}) 41 | out["results"] = resultsOut 42 | out["summary"] = summary 43 | 44 | j, _ := json.MarshalIndent(out, "", " ") 45 | fmt.Fprintln(w, string(j)) 46 | 47 | if failed > 0 { 48 | return 1 49 | } 50 | 51 | return 0 52 | } 53 | 54 | func init() { 55 | RegisterOutputer("json", &Json{}) 56 | } 57 | 58 | func struct2map(i interface{}) map[string]interface{} { 59 | out := make(map[string]interface{}) 60 | j, _ := json.Marshal(i) 61 | json.Unmarshal(j, &out) 62 | return out 63 | } 64 | -------------------------------------------------------------------------------- /resource/group.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aelsabbahy/goss/system" 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type Group struct { 11 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 12 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 13 | Groupname string `json:"-" yaml:"-"` 14 | Exists matcher `json:"exists" yaml:"exists"` 15 | GID matcher `json:"gid,omitempty" yaml:"gid,omitempty"` 16 | } 17 | 18 | func (g *Group) ID() string { return g.Groupname } 19 | func (g *Group) SetID(id string) { g.Groupname = id } 20 | 21 | func (g *Group) GetTitle() string { return g.Title } 22 | func (g *Group) GetMeta() meta { return g.Meta } 23 | 24 | func (g *Group) Validate(sys *system.System) []TestResult { 25 | skip := false 26 | sysgroup := sys.NewGroup(g.Groupname, sys, util.Config{}) 27 | 28 | var results []TestResult 29 | results = append(results, ValidateValue(g, "exists", g.Exists, sysgroup.Exists, skip)) 30 | if shouldSkip(results) { 31 | skip = true 32 | } 33 | if g.GID != nil { 34 | gGID := deprecateAtoI(g.GID, fmt.Sprintf("%s: group.gid", g.Groupname)) 35 | results = append(results, ValidateValue(g, "gid", gGID, sysgroup.GID, skip)) 36 | } 37 | return results 38 | } 39 | 40 | func NewGroup(sysGroup system.Group, config util.Config) (*Group, error) { 41 | groupname := sysGroup.Groupname() 42 | exists, _ := sysGroup.Exists() 43 | g := &Group{ 44 | Groupname: groupname, 45 | Exists: exists, 46 | } 47 | if !contains(config.IgnoreList, "stderr") { 48 | if gid, err := sysGroup.GID(); err == nil { 49 | g.GID = gid 50 | } 51 | } 52 | return g, nil 53 | } 54 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | func mkSlice(args ...interface{}) []interface{} { 15 | return args 16 | } 17 | 18 | func readFile(f string) (string, error) { 19 | b, err := ioutil.ReadFile(f) 20 | if err != nil { 21 | return "", err 22 | 23 | } 24 | return strings.TrimSpace(string(b)), nil 25 | } 26 | 27 | func getEnv(key string, def ...string) string { 28 | val := os.Getenv(key) 29 | if val == "" && len(def) > 0 { 30 | return def[0] 31 | } 32 | 33 | return os.Getenv(key) 34 | } 35 | 36 | func regexMatch(re, s string) (bool, error) { 37 | compiled, err := regexp.Compile(re) 38 | if err != nil { 39 | return false, err 40 | } 41 | 42 | return compiled.MatchString(s), nil 43 | } 44 | 45 | var funcMap = map[string]interface{}{ 46 | "mkSlice": mkSlice, 47 | "readFile": readFile, 48 | "getEnv": getEnv, 49 | "regexMatch": regexMatch, 50 | } 51 | 52 | func NewTemplateFilter(varsFile string) func([]byte) []byte { 53 | vars, err := varsFromFile(varsFile) 54 | if err != nil { 55 | fmt.Printf("Error: loading vars file '%s'\n%v\n", varsFile, err) 56 | os.Exit(1) 57 | } 58 | tVars := &TmplVars{Vars: vars} 59 | 60 | f := func(data []byte) []byte { 61 | funcMap := funcMap 62 | t := template.New("test").Funcs(template.FuncMap(funcMap)) 63 | tmpl, err := t.Parse(string(data)) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | tmpl.Option("missingkey=error") 68 | var doc bytes.Buffer 69 | err = tmpl.Execute(&doc, tVars) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | return doc.Bytes() 74 | } 75 | return f 76 | } 77 | -------------------------------------------------------------------------------- /system/service_systemd.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type ServiceSystemd struct { 11 | service string 12 | } 13 | 14 | func NewServiceSystemd(service string, system *System, config util.Config) Service { 15 | return &ServiceSystemd{ 16 | service: service, 17 | } 18 | } 19 | 20 | func (s *ServiceSystemd) Service() string { 21 | return s.service 22 | } 23 | 24 | func (s *ServiceSystemd) Exists() (bool, error) { 25 | if invalidService(s.service) { 26 | return false, nil 27 | } 28 | cmd := util.NewCommand("systemctl", "-q", "list-unit-files", "--type=service") 29 | cmd.Run() 30 | if strings.Contains(cmd.Stdout.String(), fmt.Sprintf("%s.service", s.service)) { 31 | return true, cmd.Err 32 | } 33 | // Fallback on sysv 34 | sysv := &ServiceInit{service: s.service} 35 | if e, err := sysv.Exists(); e && err == nil { 36 | return true, nil 37 | } 38 | return false, nil 39 | } 40 | 41 | func (s *ServiceSystemd) Enabled() (bool, error) { 42 | if invalidService(s.service) { 43 | return false, nil 44 | } 45 | cmd := util.NewCommand("systemctl", "-q", "is-enabled", s.service) 46 | cmd.Run() 47 | if cmd.Status == 0 { 48 | return true, cmd.Err 49 | } 50 | // Fallback on sysv 51 | sysv := &ServiceInit{service: s.service} 52 | if en, err := sysv.Enabled(); en && err == nil { 53 | return true, nil 54 | } 55 | return false, nil 56 | } 57 | 58 | func (s *ServiceSystemd) Running() (bool, error) { 59 | if invalidService(s.service) { 60 | return false, nil 61 | } 62 | cmd := util.NewCommand("systemctl", "-q", "is-active", s.service) 63 | cmd.Run() 64 | if cmd.Status == 0 { 65 | return true, cmd.Err 66 | } 67 | // Fallback on sysv 68 | sysv := &ServiceInit{service: s.service} 69 | if r, err := sysv.Running(); r && err == nil { 70 | return true, nil 71 | } 72 | return false, nil 73 | } 74 | -------------------------------------------------------------------------------- /system/service_init.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/aelsabbahy/goss/util" 9 | ) 10 | 11 | type ServiceInit struct { 12 | service string 13 | alpine bool 14 | } 15 | 16 | func NewServiceInit(service string, system *System, config util.Config) Service { 17 | return &ServiceInit{service: service} 18 | } 19 | 20 | func NewAlpineServiceInit(service string, system *System, config util.Config) Service { 21 | return &ServiceInit{service: service, alpine: true} 22 | } 23 | 24 | func (s *ServiceInit) Service() string { 25 | return s.service 26 | } 27 | 28 | func (s *ServiceInit) Exists() (bool, error) { 29 | if invalidService(s.service) { 30 | return false, nil 31 | } 32 | if _, err := os.Stat(fmt.Sprintf("/etc/init.d/%s", s.service)); err == nil { 33 | return true, err 34 | } 35 | return false, nil 36 | } 37 | 38 | func (s *ServiceInit) Enabled() (bool, error) { 39 | if invalidService(s.service) { 40 | return false, nil 41 | } 42 | if s.alpine { 43 | return alpineInitServiceEnabled(s.service, "sysinit") 44 | } else { 45 | return initServiceEnabled(s.service, 3) 46 | } 47 | } 48 | 49 | func (s *ServiceInit) Running() (bool, error) { 50 | if invalidService(s.service) { 51 | return false, nil 52 | } 53 | cmd := util.NewCommand("service", s.service, "status") 54 | cmd.Run() 55 | if cmd.Status == 0 { 56 | return true, cmd.Err 57 | } 58 | return false, nil 59 | } 60 | 61 | func initServiceEnabled(service string, level int) (bool, error) { 62 | matches, err := filepath.Glob(fmt.Sprintf("/etc/rc%d.d/S[0-9][0-9]%s", level, service)) 63 | if err == nil && matches != nil { 64 | return true, nil 65 | } 66 | return false, err 67 | } 68 | 69 | func alpineInitServiceEnabled(service string, level string) (bool, error) { 70 | matches, err := filepath.Glob(fmt.Sprintf("/etc/runlevels/%s/%s", level, service)) 71 | if err == nil && matches != nil { 72 | return true, nil 73 | } 74 | return false, err 75 | } 76 | -------------------------------------------------------------------------------- /resource/dns.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/aelsabbahy/goss/system" 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type DNS struct { 11 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 12 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 13 | Host string `json:"-" yaml:"-"` 14 | Resolveable matcher `json:"resolveable" yaml:"resolveable"` 15 | Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"` 16 | Timeout int `json:"timeout" yaml:"timeout"` 17 | Server string `json:"server,omitempty" yaml:"server,omitempty"` 18 | } 19 | 20 | func (d *DNS) ID() string { return d.Host } 21 | func (d *DNS) SetID(id string) { d.Host = id } 22 | 23 | func (d *DNS) GetTitle() string { return d.Title } 24 | func (d *DNS) GetMeta() meta { return d.Meta } 25 | 26 | func (d *DNS) Validate(sys *system.System) []TestResult { 27 | skip := false 28 | if d.Timeout == 0 { 29 | d.Timeout = 500 30 | } 31 | 32 | sysDNS := sys.NewDNS(d.Host, sys, util.Config{Timeout: d.Timeout, Server: d.Server}) 33 | 34 | var results []TestResult 35 | results = append(results, ValidateValue(d, "resolveable", d.Resolveable, sysDNS.Resolveable, skip)) 36 | if shouldSkip(results) { 37 | skip = true 38 | } 39 | if d.Addrs != nil { 40 | results = append(results, ValidateValue(d, "addrs", d.Addrs, sysDNS.Addrs, skip)) 41 | } 42 | return results 43 | } 44 | 45 | func NewDNS(sysDNS system.DNS, config util.Config) (*DNS, error) { 46 | var host string 47 | if sysDNS.Qtype() != "" { 48 | host = strings.Join([]string{sysDNS.Qtype(), sysDNS.Host()}, ":") 49 | } else { 50 | host = sysDNS.Host() 51 | } 52 | 53 | resolveable, err := sysDNS.Resolveable() 54 | server := sysDNS.Server() 55 | 56 | d := &DNS{ 57 | Host: host, 58 | Resolveable: resolveable, 59 | Timeout: config.Timeout, 60 | Server: server, 61 | } 62 | if !contains(config.IgnoreList, "addrs") { 63 | addrs, _ := sysDNS.Addrs() 64 | d.Addrs = addrs 65 | } 66 | return d, err 67 | } 68 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: ee9c9147007d86588eb760fe7985f4017b3798255d99b23d3240c6a0d8b33291 2 | updated: 2016-11-09T02:23:29.857676716Z 3 | imports: 4 | - name: github.com/achanda/go-sysctl 5 | version: 6be7678c45d2052640e72060e4f5db6165b1ecab 6 | - name: github.com/aelsabbahy/GOnetstat 7 | version: edf89f784e0876818dc19f7744a16742a0a66f16 8 | - name: github.com/cheekybits/genny 9 | version: e8e29e67948b15c64e60d6617182c18cf7eead7f 10 | subpackages: 11 | - generic 12 | - name: github.com/docker/docker 13 | version: 383a2f046b16c9f79d2fb800844e6550cc784871 14 | subpackages: 15 | - pkg/mount 16 | - name: github.com/fatih/color 17 | version: bf82308e8c8546dc2b945157173eb8a959ae9505 18 | - name: github.com/mattn/go-colorable 19 | version: d228849504861217f796da67fae4f6e347643f15 20 | - name: github.com/mattn/go-isatty 21 | version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 22 | - name: github.com/miekg/dns 23 | version: 58f52c57ce9df13460ac68200cef30a008b9c468 24 | - name: github.com/mitchellh/go-ps 25 | version: e2d21980687ce16e58469d98dcee92d27fbbd7fb 26 | - name: github.com/oleiade/reflections 27 | version: 0e86b3c98b2ff33e30c85cfe97d9a63d439fe7eb 28 | - name: github.com/onsi/gomega 29 | version: ff4bc6b6f9f5affa66635cd04d31d2a7ee21ffd6 30 | subpackages: 31 | - types 32 | - internal/assertion 33 | - internal/asyncassertion 34 | - internal/testingtsupport 35 | - matchers 36 | - internal/oraclematcher 37 | - format 38 | - matchers/support/goraph/bipartitegraph 39 | - matchers/support/goraph/edge 40 | - matchers/support/goraph/node 41 | - matchers/support/goraph/util 42 | - name: github.com/opencontainers/runc 43 | version: 8779fa57eb4a810a7360187dfa5e168a76cf5d21 44 | subpackages: 45 | - libcontainer/user 46 | - name: github.com/patrickmn/go-cache 47 | version: 1881a9bccb818787f68c52bfba648c6cf34c34fa 48 | - name: github.com/urfave/cli 49 | version: d86a009f5e13f83df65d0d6cee9a2e3f1445f0da 50 | - name: golang.org/x/sys 51 | version: 9a2e24c3733eddc63871eda99f253e2db29bd3b9 52 | subpackages: 53 | - unix 54 | - name: gopkg.in/yaml.v2 55 | version: a5b47d31c556af34a302ce5d659e6fea44d90de0 56 | testImports: [] 57 | -------------------------------------------------------------------------------- /resource/http.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type HTTP struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | HTTP string `json:"-" yaml:"-"` 12 | Status matcher `json:"status" yaml:"status"` 13 | AllowInsecure bool `json:"allow-insecure" yaml:"allow-insecure"` 14 | NoFollowRedirects bool `json:"no-follow-redirects" yaml:"no-follow-redirects"` 15 | Timeout int `json:"timeout" yaml:"timeout"` 16 | Body []string `json:"body" yaml:"body"` 17 | } 18 | 19 | func (u *HTTP) ID() string { return u.HTTP } 20 | func (u *HTTP) SetID(id string) { u.HTTP = id } 21 | 22 | // FIXME: Can this be refactored? 23 | func (r *HTTP) GetTitle() string { return r.Title } 24 | func (r *HTTP) GetMeta() meta { return r.Meta } 25 | 26 | func (u *HTTP) Validate(sys *system.System) []TestResult { 27 | skip := false 28 | if u.Timeout == 0 { 29 | u.Timeout = 5000 30 | } 31 | sysHTTP := sys.NewHTTP(u.HTTP, sys, util.Config{AllowInsecure: u.AllowInsecure, NoFollowRedirects: u.NoFollowRedirects, Timeout: u.Timeout}) 32 | sysHTTP.SetAllowInsecure(u.AllowInsecure) 33 | sysHTTP.SetNoFollowRedirects(u.NoFollowRedirects) 34 | 35 | var results []TestResult 36 | results = append(results, ValidateValue(u, "status", u.Status, sysHTTP.Status, skip)) 37 | if shouldSkip(results) { 38 | skip = true 39 | } 40 | if len(u.Body) > 0 { 41 | results = append(results, ValidateContains(u, "Body", u.Body, sysHTTP.Body, skip)) 42 | } 43 | 44 | return results 45 | } 46 | 47 | func NewHTTP(sysHTTP system.HTTP, config util.Config) (*HTTP, error) { 48 | http := sysHTTP.HTTP() 49 | status, err := sysHTTP.Status() 50 | u := &HTTP{ 51 | HTTP: http, 52 | Status: status, 53 | Body: []string{}, 54 | AllowInsecure: config.AllowInsecure, 55 | NoFollowRedirects: config.NoFollowRedirects, 56 | Timeout: config.Timeout, 57 | } 58 | return u, err 59 | } 60 | -------------------------------------------------------------------------------- /system/service_upstart.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/aelsabbahy/goss/util" 11 | ) 12 | 13 | type ServiceUpstart struct { 14 | service string 15 | } 16 | 17 | var upstartEnabled = regexp.MustCompile(`^\s*start on`) 18 | var upstartDisabled = regexp.MustCompile(`^manual`) 19 | 20 | func NewServiceUpstart(service string, system *System, config util.Config) Service { 21 | return &ServiceUpstart{service: service} 22 | } 23 | 24 | func (s *ServiceUpstart) Service() string { 25 | return s.service 26 | } 27 | 28 | func (s *ServiceUpstart) Exists() (bool, error) { 29 | // upstart 30 | if _, err := os.Stat(fmt.Sprintf("/etc/init/%s.conf", s.service)); err == nil { 31 | return true, nil 32 | } 33 | // Fallback on sysv 34 | sysv := &ServiceInit{service: s.service} 35 | if e, err := sysv.Exists(); e && err == nil { 36 | return true, nil 37 | } 38 | return false, nil 39 | } 40 | 41 | func (s *ServiceUpstart) Enabled() (bool, error) { 42 | if fh, err := os.Open(fmt.Sprintf("/etc/init/%s.override", s.service)); err == nil { 43 | scanner := bufio.NewScanner(fh) 44 | for scanner.Scan() { 45 | line := scanner.Text() 46 | if upstartDisabled.MatchString(line) { 47 | return false, nil 48 | } 49 | } 50 | } 51 | 52 | // If no /etc/init/.override with `manual` keyword in it has been found 53 | // Check the contents of the upstart manifest. 54 | if fh, err := os.Open(fmt.Sprintf("/etc/init/%s.conf", s.service)); err == nil { 55 | scanner := bufio.NewScanner(fh) 56 | for scanner.Scan() { 57 | line := scanner.Text() 58 | if upstartEnabled.MatchString(line) { 59 | return true, nil 60 | } 61 | } 62 | } 63 | // Fallback on sysv 64 | sysv := &ServiceInit{service: s.service} 65 | if en, err := sysv.Enabled(); en && err == nil { 66 | return true, nil 67 | } 68 | return false, nil 69 | } 70 | 71 | func (s *ServiceUpstart) Running() (bool, error) { 72 | cmd := util.NewCommand("service", s.service, "status") 73 | cmd.Run() 74 | out := cmd.Stdout.String() 75 | if cmd.Status == 0 && (strings.Contains(out, "running") || strings.Contains(out, "online")) { 76 | return true, cmd.Err 77 | } 78 | return false, nil 79 | } 80 | -------------------------------------------------------------------------------- /system/mount.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | "github.com/docker/docker/pkg/mount" 9 | ) 10 | 11 | type Mount interface { 12 | MountPoint() string 13 | Exists() (bool, error) 14 | Opts() ([]string, error) 15 | Source() (string, error) 16 | Filesystem() (string, error) 17 | } 18 | 19 | type DefMount struct { 20 | mountPoint string 21 | loaded bool 22 | exists bool 23 | mountInfo *mount.Info 24 | err error 25 | } 26 | 27 | func NewDefMount(mountPoint string, system *System, config util.Config) Mount { 28 | return &DefMount{ 29 | mountPoint: mountPoint, 30 | } 31 | } 32 | 33 | func (m *DefMount) setup() error { 34 | if m.loaded { 35 | return m.err 36 | } 37 | m.loaded = true 38 | 39 | mountInfo, err := getMount(m.mountPoint) 40 | if err != nil { 41 | m.exists = false 42 | m.err = err 43 | return m.err 44 | } 45 | m.mountInfo = mountInfo 46 | m.exists = true 47 | return nil 48 | } 49 | 50 | func (m *DefMount) ID() string { 51 | return m.mountPoint 52 | } 53 | 54 | func (m *DefMount) MountPoint() string { 55 | return m.mountPoint 56 | } 57 | 58 | func (m *DefMount) Exists() (bool, error) { 59 | if err := m.setup(); err != nil { 60 | return false, nil 61 | } 62 | 63 | return m.exists, nil 64 | } 65 | 66 | func (m *DefMount) Opts() ([]string, error) { 67 | if err := m.setup(); err != nil { 68 | return nil, err 69 | } 70 | 71 | return strings.Split(m.mountInfo.Opts, ","), nil 72 | } 73 | 74 | func (m *DefMount) Source() (string, error) { 75 | if err := m.setup(); err != nil { 76 | return "", err 77 | } 78 | 79 | return m.mountInfo.Source, nil 80 | } 81 | 82 | func (m *DefMount) Filesystem() (string, error) { 83 | if err := m.setup(); err != nil { 84 | return "", err 85 | } 86 | 87 | return m.mountInfo.Fstype, nil 88 | } 89 | 90 | func getMount(mountpoint string) (*mount.Info, error) { 91 | entries, err := mount.GetMounts() 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | // Search the table for the mountpoint 97 | for _, e := range entries { 98 | if e.Mountpoint == mountpoint { 99 | return e, nil 100 | } 101 | } 102 | return nil, fmt.Errorf("Mountpoint not found") 103 | } 104 | -------------------------------------------------------------------------------- /resource/mount.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type Mount struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | MountPoint string `json:"-" yaml:"-"` 12 | Exists matcher `json:"exists" yaml:"exists"` 13 | Opts matcher `json:"opts,omitempty" yaml:"opts,omitempty"` 14 | Source matcher `json:"source,omitempty" yaml:"source,omitempty"` 15 | Filesystem matcher `json:"filesystem,omitempty" yaml:"filesystem,omitempty"` 16 | } 17 | 18 | func (m *Mount) ID() string { return m.MountPoint } 19 | func (m *Mount) SetID(id string) { m.MountPoint = id } 20 | 21 | // FIXME: Can this be refactored? 22 | func (m *Mount) GetTitle() string { return m.Title } 23 | func (m *Mount) GetMeta() meta { return m.Meta } 24 | 25 | func (m *Mount) Validate(sys *system.System) []TestResult { 26 | skip := false 27 | sysMount := sys.NewMount(m.MountPoint, sys, util.Config{}) 28 | 29 | var results []TestResult 30 | results = append(results, ValidateValue(m, "exists", m.Exists, sysMount.Exists, skip)) 31 | if shouldSkip(results) { 32 | skip = true 33 | } 34 | if m.Opts != nil { 35 | results = append(results, ValidateValue(m, "opts", m.Opts, sysMount.Opts, skip)) 36 | } 37 | if m.Source != nil { 38 | results = append(results, ValidateValue(m, "source", m.Source, sysMount.Source, skip)) 39 | } 40 | if m.Filesystem != nil { 41 | results = append(results, ValidateValue(m, "filesystem", m.Filesystem, sysMount.Filesystem, skip)) 42 | } 43 | return results 44 | } 45 | 46 | func NewMount(sysMount system.Mount, config util.Config) (*Mount, error) { 47 | mountPoint := sysMount.MountPoint() 48 | exists, _ := sysMount.Exists() 49 | m := &Mount{ 50 | MountPoint: mountPoint, 51 | Exists: exists, 52 | } 53 | if !contains(config.IgnoreList, "opts") { 54 | if opts, err := sysMount.Opts(); err == nil { 55 | m.Opts = opts 56 | } 57 | } 58 | if !contains(config.IgnoreList, "source") { 59 | if source, err := sysMount.Source(); err == nil { 60 | m.Source = source 61 | } 62 | } 63 | if !contains(config.IgnoreList, "filesystem") { 64 | if filesystem, err := sysMount.Filesystem(); err == nil { 65 | m.Filesystem = filesystem 66 | } 67 | } 68 | return m, nil 69 | } 70 | -------------------------------------------------------------------------------- /system/command.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | "time" 9 | 10 | "github.com/aelsabbahy/goss/util" 11 | ) 12 | 13 | type Command interface { 14 | Command() string 15 | Exists() (bool, error) 16 | ExitStatus() (int, error) 17 | Stdout() (io.Reader, error) 18 | Stderr() (io.Reader, error) 19 | } 20 | 21 | type DefCommand struct { 22 | command string 23 | exitStatus int 24 | stdout io.Reader 25 | stderr io.Reader 26 | loaded bool 27 | Timeout int 28 | err error 29 | } 30 | 31 | func NewDefCommand(command string, system *System, config util.Config) Command { 32 | return &DefCommand{ 33 | command: command, 34 | Timeout: config.Timeout, 35 | } 36 | } 37 | 38 | func (c *DefCommand) setup() error { 39 | if c.loaded { 40 | return c.err 41 | } 42 | c.loaded = true 43 | 44 | cmd := util.NewCommand("sh", "-c", c.command) 45 | err := runCommand(cmd, c.Timeout) 46 | 47 | // We don't care about ExitError since it's covered by status 48 | if _, ok := err.(*exec.ExitError); !ok { 49 | c.err = err 50 | } 51 | c.exitStatus = cmd.Status 52 | c.stdout = bytes.NewReader(cmd.Stdout.Bytes()) 53 | c.stderr = bytes.NewReader(cmd.Stderr.Bytes()) 54 | 55 | return c.err 56 | } 57 | 58 | func (c *DefCommand) Command() string { 59 | return c.command 60 | } 61 | 62 | func (c *DefCommand) ExitStatus() (int, error) { 63 | err := c.setup() 64 | 65 | return c.exitStatus, err 66 | } 67 | 68 | func (c *DefCommand) Stdout() (io.Reader, error) { 69 | err := c.setup() 70 | 71 | return c.stdout, err 72 | } 73 | 74 | func (c *DefCommand) Stderr() (io.Reader, error) { 75 | err := c.setup() 76 | 77 | return c.stderr, err 78 | } 79 | 80 | // Stub out 81 | func (c *DefCommand) Exists() (bool, error) { 82 | return false, nil 83 | } 84 | 85 | func runCommand(cmd *util.Command, timeout int) error { 86 | c1 := make(chan bool, 1) 87 | e1 := make(chan error, 1) 88 | timeoutD := time.Duration(timeout) * time.Millisecond 89 | go func() { 90 | err := cmd.Run() 91 | if err != nil { 92 | e1 <- err 93 | } 94 | c1 <- true 95 | }() 96 | select { 97 | case <-c1: 98 | return nil 99 | case err := <-e1: 100 | return err 101 | case <-time.After(timeoutD): 102 | return fmt.Errorf("Command execution timed out (%s)", timeoutD) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /system/http.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/aelsabbahy/goss/util" 10 | ) 11 | 12 | type HTTP interface { 13 | HTTP() string 14 | Status() (int, error) 15 | Body() (io.Reader, error) 16 | Exists() (bool, error) 17 | SetAllowInsecure(bool) 18 | SetNoFollowRedirects(bool) 19 | } 20 | 21 | type DefHTTP struct { 22 | http string 23 | allowInsecure bool 24 | noFollowRedirects bool 25 | resp *http.Response 26 | Timeout int 27 | loaded bool 28 | err error 29 | } 30 | 31 | func NewDefHTTP(http string, system *System, config util.Config) HTTP { 32 | return &DefHTTP{ 33 | http: http, 34 | allowInsecure: config.AllowInsecure, 35 | noFollowRedirects: config.NoFollowRedirects, 36 | Timeout: config.Timeout, 37 | } 38 | } 39 | 40 | func (u *DefHTTP) setup() error { 41 | if u.loaded { 42 | return u.err 43 | } 44 | u.loaded = true 45 | 46 | tr := &http.Transport{ 47 | TLSClientConfig: &tls.Config{InsecureSkipVerify: u.allowInsecure}, 48 | DisableKeepAlives: true, 49 | } 50 | client := &http.Client{ 51 | Transport: tr, 52 | Timeout: time.Duration(u.Timeout) * time.Millisecond, 53 | } 54 | 55 | if u.noFollowRedirects { 56 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 57 | return http.ErrUseLastResponse 58 | } 59 | } 60 | u.resp, u.err = client.Get(u.http) 61 | 62 | return u.err 63 | } 64 | 65 | func (u *DefHTTP) Exists() (bool, error) { 66 | if _, err := u.Status(); err != nil { 67 | return false, err 68 | } 69 | return true, nil 70 | } 71 | 72 | func (u *DefHTTP) SetNoFollowRedirects(t bool) { 73 | u.noFollowRedirects = t 74 | } 75 | 76 | func (u *DefHTTP) SetAllowInsecure(t bool) { 77 | u.allowInsecure = t 78 | } 79 | 80 | func (u *DefHTTP) ID() string { 81 | return u.http 82 | } 83 | func (u *DefHTTP) HTTP() string { 84 | return u.http 85 | } 86 | 87 | func (u *DefHTTP) Status() (int, error) { 88 | if err := u.setup(); err != nil { 89 | return 0, err 90 | } 91 | 92 | return u.resp.StatusCode, nil 93 | } 94 | 95 | func (u *DefHTTP) Body() (io.Reader, error) { 96 | if err := u.setup(); err != nil { 97 | return nil, err 98 | } 99 | 100 | return u.resp.Body, nil 101 | } 102 | -------------------------------------------------------------------------------- /outputs/junit.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/aelsabbahy/goss/resource" 12 | "github.com/fatih/color" 13 | ) 14 | 15 | type JUnit struct{} 16 | 17 | func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time) (exitCode int) { 18 | color.NoColor = true 19 | var testCount, failed, skipped int 20 | 21 | // ISO8601 timeformat 22 | timestamp := time.Now().Format(time.RFC3339) 23 | 24 | var summary map[int]string 25 | summary = make(map[int]string) 26 | 27 | for resultGroup := range results { 28 | for _, testResult := range resultGroup { 29 | m := struct2map(testResult) 30 | duration := strconv.FormatFloat(m["duration"].(float64)/1000/1000/1000, 'f', 3, 64) 31 | summary[testCount] = "\n" 36 | if testResult.Result == resource.FAIL { 37 | summary[testCount] += "" + 38 | escapeString(humanizeResult2(testResult)) + 39 | "\n" 40 | summary[testCount] += "" + 41 | escapeString(humanizeResult2(testResult)) + 42 | "\n\n" 43 | 44 | failed++ 45 | } else { 46 | if testResult.Result == resource.SKIP { 47 | summary[testCount] += "" 48 | skipped++ 49 | } 50 | summary[testCount] += "" + 51 | escapeString(humanizeResult2(testResult)) + 52 | "\n\n" 53 | } 54 | testCount++ 55 | } 56 | } 57 | 58 | duration := time.Since(startTime) 59 | fmt.Fprintln(w, "") 60 | fmt.Fprintf(w, "\n", 62 | testCount, failed, skipped, duration.Seconds(), timestamp) 63 | 64 | for i := 0; i < testCount; i++ { 65 | fmt.Fprintf(w, "%s", summary[i]) 66 | } 67 | 68 | fmt.Fprintln(w, "") 69 | 70 | if failed > 0 { 71 | return 1 72 | } 73 | 74 | return 0 75 | } 76 | 77 | func init() { 78 | RegisterOutputer("junit", &JUnit{}) 79 | } 80 | 81 | func escapeString(str string) string { 82 | buffer := new(bytes.Buffer) 83 | xml.EscapeText(buffer, []byte(str)) 84 | return buffer.String() 85 | } 86 | -------------------------------------------------------------------------------- /serve.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | "github.com/aelsabbahy/goss/outputs" 11 | "github.com/aelsabbahy/goss/system" 12 | "github.com/fatih/color" 13 | "github.com/patrickmn/go-cache" 14 | "github.com/urfave/cli" 15 | ) 16 | 17 | func Serve(c *cli.Context) { 18 | endpoint := c.String("endpoint") 19 | color.NoColor = true 20 | cache := cache.New(c.Duration("cache"), 30*time.Second) 21 | 22 | health := healthHandler{ 23 | c: c, 24 | gossConfig: getGossConfig(c), 25 | sys: system.New(c), 26 | outputer: getOutputer(c), 27 | cache: cache, 28 | gossMu: &sync.Mutex{}, 29 | maxConcurrent: c.Int("max-concurrent"), 30 | } 31 | if c.String("format") == "json" { 32 | health.contentType = "application/json" 33 | } 34 | http.Handle(endpoint, health) 35 | listenAddr := c.String("listen-addr") 36 | log.Printf("Starting to listen on: %s", listenAddr) 37 | log.Fatal(http.ListenAndServe(c.String("listen-addr"), nil)) 38 | } 39 | 40 | type res struct { 41 | exitCode int 42 | b bytes.Buffer 43 | } 44 | type healthHandler struct { 45 | c *cli.Context 46 | gossConfig GossConfig 47 | sys *system.System 48 | outputer outputs.Outputer 49 | cache *cache.Cache 50 | gossMu *sync.Mutex 51 | contentType string 52 | maxConcurrent int 53 | } 54 | 55 | func (h healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 56 | log.Printf("%v: requesting health probe", r.RemoteAddr) 57 | var resp res 58 | tmp, found := h.cache.Get("res") 59 | if found { 60 | resp = tmp.(res) 61 | } else { 62 | h.gossMu.Lock() 63 | defer h.gossMu.Unlock() 64 | tmp, found := h.cache.Get("res") 65 | if found { 66 | resp = tmp.(res) 67 | } else { 68 | h.sys = system.New(h.c) 69 | log.Printf("%v: Stale cache, running tests", r.RemoteAddr) 70 | iStartTime := time.Now() 71 | out := validate(h.sys, h.gossConfig, h.maxConcurrent) 72 | var b bytes.Buffer 73 | exitCode := h.outputer.Output(&b, out, iStartTime) 74 | resp = res{exitCode: exitCode, b: b} 75 | h.cache.Set("res", resp, cache.DefaultExpiration) 76 | } 77 | } 78 | if h.contentType != "" { 79 | w.Header().Set("Content-Type", h.contentType) 80 | } 81 | if resp.exitCode == 0 { 82 | resp.b.WriteTo(w) 83 | } else { 84 | w.WriteHeader(http.StatusServiceUnavailable) 85 | resp.b.WriteTo(w) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /integration-tests/goss/centos7/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contains: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contains: [] 8 | package: 9 | foobar: 10 | installed: false 11 | httpd: 12 | installed: true 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://google.com:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://google.com:443: 20 | reachable: true 21 | timeout: 1000 22 | port: 23 | tcp:80: 24 | listening: false 25 | tcp:9999: 26 | listening: false 27 | tcp6:80: 28 | listening: true 29 | service: 30 | foobar: 31 | enabled: false 32 | running: false 33 | httpd: 34 | enabled: true 35 | running: true 36 | user: 37 | apache: 38 | exists: true 39 | foobar: 40 | exists: false 41 | group: 42 | apache: 43 | exists: true 44 | foobar: 45 | exists: false 46 | command: 47 | echo 'hi': 48 | exit-status: 0 49 | stdout: [] 50 | stderr: [] 51 | timeout: 10000 52 | foobar: 53 | exit-status: 127 54 | stdout: [] 55 | stderr: [] 56 | timeout: 10000 57 | dns: 58 | CAA:dnstest.io: 59 | resolveable: true 60 | timeout: 1000 61 | server: 8.8.8.8 62 | CNAME:c.dnstest.io: 63 | resolveable: true 64 | timeout: 1000 65 | server: 8.8.8.8 66 | MX:dnstest.io: 67 | resolveable: true 68 | timeout: 1000 69 | server: 8.8.8.8 70 | NS:dnstest.io: 71 | resolveable: true 72 | timeout: 1000 73 | server: 8.8.8.8 74 | PTR:8.8.8.8: 75 | resolveable: true 76 | timeout: 1000 77 | server: 8.8.8.8 78 | SRV:_https._tcp.dnstest.io: 79 | resolveable: true 80 | timeout: 1000 81 | server: 8.8.8.8 82 | TXT:txt._test.dnstest.io: 83 | resolveable: true 84 | timeout: 1000 85 | server: 8.8.8.8 86 | ip6.dnstest.io: 87 | resolveable: true 88 | timeout: 1000 89 | server: 8.8.8.8 90 | localhost: 91 | resolveable: true 92 | timeout: 1000 93 | process: 94 | foobar: 95 | running: false 96 | httpd: 97 | running: true 98 | kernel-param: 99 | kernel.ostype: 100 | value: Linux 101 | mount: 102 | /dev: 103 | exists: true 104 | http: 105 | http://google.com: 106 | status: 301 107 | allow-insecure: false 108 | no-follow-redirects: true 109 | timeout: 5000 110 | body: [] 111 | https://www.google.com: 112 | status: 200 113 | allow-insecure: false 114 | no-follow-redirects: false 115 | timeout: 5000 116 | body: [] 117 | -------------------------------------------------------------------------------- /integration-tests/goss/alpine3/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contains: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contains: [] 8 | package: 9 | apache2: 10 | installed: true 11 | foobar: 12 | installed: false 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://google.com:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://google.com:443: 20 | reachable: true 21 | timeout: 1000 22 | port: 23 | tcp:80: 24 | listening: false 25 | tcp:9999: 26 | listening: false 27 | tcp6:80: 28 | listening: true 29 | service: 30 | apache2: 31 | enabled: true 32 | running: true 33 | foobar: 34 | enabled: false 35 | running: false 36 | user: 37 | foobar: 38 | exists: false 39 | www-data: 40 | exists: false 41 | group: 42 | foobar: 43 | exists: false 44 | www-data: 45 | exists: true 46 | command: 47 | echo 'hi': 48 | exit-status: 0 49 | stdout: [] 50 | stderr: [] 51 | timeout: 10000 52 | foobar: 53 | exit-status: 127 54 | stdout: [] 55 | stderr: [] 56 | timeout: 10000 57 | dns: 58 | CAA:dnstest.io: 59 | resolveable: true 60 | timeout: 1000 61 | server: 8.8.8.8 62 | CNAME:c.dnstest.io: 63 | resolveable: true 64 | timeout: 1000 65 | server: 8.8.8.8 66 | MX:dnstest.io: 67 | resolveable: true 68 | timeout: 1000 69 | server: 8.8.8.8 70 | NS:dnstest.io: 71 | resolveable: true 72 | timeout: 1000 73 | server: 8.8.8.8 74 | PTR:8.8.8.8: 75 | resolveable: true 76 | timeout: 1000 77 | server: 8.8.8.8 78 | SRV:_https._tcp.dnstest.io: 79 | resolveable: true 80 | timeout: 1000 81 | server: 8.8.8.8 82 | TXT:txt._test.dnstest.io: 83 | resolveable: true 84 | timeout: 1000 85 | server: 8.8.8.8 86 | ip6.dnstest.io: 87 | resolveable: true 88 | timeout: 1000 89 | server: 8.8.8.8 90 | localhost: 91 | resolveable: true 92 | timeout: 1000 93 | process: 94 | apache2: 95 | running: false 96 | foobar: 97 | running: false 98 | kernel-param: 99 | kernel.ostype: 100 | value: Linux 101 | mount: 102 | /dev: 103 | exists: true 104 | http: 105 | http://google.com: 106 | status: 301 107 | allow-insecure: false 108 | no-follow-redirects: true 109 | timeout: 5000 110 | body: [] 111 | https://www.google.com: 112 | status: 200 113 | allow-insecure: false 114 | no-follow-redirects: false 115 | timeout: 5000 116 | body: [] 117 | -------------------------------------------------------------------------------- /integration-tests/goss/precise/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contains: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contains: [] 8 | package: 9 | apache2: 10 | installed: true 11 | foobar: 12 | installed: false 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://google.com:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://google.com:443: 20 | reachable: true 21 | timeout: 1000 22 | port: 23 | tcp:80: 24 | listening: true 25 | tcp:9999: 26 | listening: false 27 | tcp6:80: 28 | listening: false 29 | service: 30 | apache2: 31 | enabled: false 32 | running: true 33 | foobar: 34 | enabled: false 35 | running: false 36 | user: 37 | foobar: 38 | exists: false 39 | www-data: 40 | exists: true 41 | group: 42 | foobar: 43 | exists: false 44 | www-data: 45 | exists: true 46 | command: 47 | echo 'hi': 48 | exit-status: 0 49 | stdout: [] 50 | stderr: [] 51 | timeout: 10000 52 | foobar: 53 | exit-status: 127 54 | stdout: [] 55 | stderr: [] 56 | timeout: 10000 57 | dns: 58 | CAA:dnstest.io: 59 | resolveable: true 60 | timeout: 1000 61 | server: 8.8.8.8 62 | CNAME:c.dnstest.io: 63 | resolveable: true 64 | timeout: 1000 65 | server: 8.8.8.8 66 | MX:dnstest.io: 67 | resolveable: true 68 | timeout: 1000 69 | server: 8.8.8.8 70 | NS:dnstest.io: 71 | resolveable: true 72 | timeout: 1000 73 | server: 8.8.8.8 74 | PTR:8.8.8.8: 75 | resolveable: true 76 | timeout: 1000 77 | server: 8.8.8.8 78 | SRV:_https._tcp.dnstest.io: 79 | resolveable: true 80 | timeout: 1000 81 | server: 8.8.8.8 82 | TXT:txt._test.dnstest.io: 83 | resolveable: true 84 | timeout: 1000 85 | server: 8.8.8.8 86 | ip6.dnstest.io: 87 | resolveable: true 88 | timeout: 1000 89 | server: 8.8.8.8 90 | localhost: 91 | resolveable: true 92 | timeout: 1000 93 | process: 94 | apache2: 95 | running: true 96 | foobar: 97 | running: false 98 | kernel-param: 99 | kernel.ostype: 100 | value: Linux 101 | mount: 102 | /dev: 103 | exists: true 104 | http: 105 | http://google.com: 106 | status: 301 107 | allow-insecure: false 108 | no-follow-redirects: true 109 | timeout: 5000 110 | body: [] 111 | https://www.google.com: 112 | status: 200 113 | allow-insecure: false 114 | no-follow-redirects: false 115 | timeout: 5000 116 | body: [] 117 | -------------------------------------------------------------------------------- /integration-tests/goss/wheezy/goss-expected-q.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | contains: [] 5 | /tmp/goss/foobar: 6 | exists: false 7 | contains: [] 8 | package: 9 | apache2: 10 | installed: true 11 | foobar: 12 | installed: false 13 | vim-tiny: 14 | installed: false 15 | addr: 16 | tcp://google.com:22: 17 | reachable: false 18 | timeout: 1000 19 | tcp://google.com:443: 20 | reachable: true 21 | timeout: 1000 22 | port: 23 | tcp:80: 24 | listening: false 25 | tcp:9999: 26 | listening: false 27 | tcp6:80: 28 | listening: true 29 | service: 30 | apache2: 31 | enabled: true 32 | running: true 33 | foobar: 34 | enabled: false 35 | running: false 36 | user: 37 | foobar: 38 | exists: false 39 | www-data: 40 | exists: true 41 | group: 42 | foobar: 43 | exists: false 44 | www-data: 45 | exists: true 46 | command: 47 | echo 'hi': 48 | exit-status: 0 49 | stdout: [] 50 | stderr: [] 51 | timeout: 10000 52 | foobar: 53 | exit-status: 127 54 | stdout: [] 55 | stderr: [] 56 | timeout: 10000 57 | dns: 58 | CAA:dnstest.io: 59 | resolveable: true 60 | timeout: 1000 61 | server: 8.8.8.8 62 | CNAME:c.dnstest.io: 63 | resolveable: true 64 | timeout: 1000 65 | server: 8.8.8.8 66 | MX:dnstest.io: 67 | resolveable: true 68 | timeout: 1000 69 | server: 8.8.8.8 70 | NS:dnstest.io: 71 | resolveable: true 72 | timeout: 1000 73 | server: 8.8.8.8 74 | PTR:8.8.8.8: 75 | resolveable: true 76 | timeout: 1000 77 | server: 8.8.8.8 78 | SRV:_https._tcp.dnstest.io: 79 | resolveable: true 80 | timeout: 1000 81 | server: 8.8.8.8 82 | TXT:txt._test.dnstest.io: 83 | resolveable: true 84 | timeout: 1000 85 | server: 8.8.8.8 86 | ip6.dnstest.io: 87 | resolveable: true 88 | timeout: 1000 89 | server: 8.8.8.8 90 | localhost: 91 | resolveable: true 92 | timeout: 1000 93 | process: 94 | apache2: 95 | running: true 96 | foobar: 97 | running: false 98 | kernel-param: 99 | kernel.ostype: 100 | value: Linux 101 | mount: 102 | /dev: 103 | exists: true 104 | http: 105 | http://google.com: 106 | status: 301 107 | allow-insecure: false 108 | no-follow-redirects: true 109 | timeout: 5000 110 | body: [] 111 | https://www.google.com: 112 | status: 200 113 | allow-insecure: false 114 | no-follow-redirects: false 115 | timeout: 5000 116 | body: [] 117 | -------------------------------------------------------------------------------- /resource/command.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/aelsabbahy/goss/system" 10 | "github.com/aelsabbahy/goss/util" 11 | ) 12 | 13 | type Command struct { 14 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 15 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 16 | Command string `json:"-" yaml:"-"` 17 | ExitStatus matcher `json:"exit-status" yaml:"exit-status"` 18 | Stdout []string `json:"stdout" yaml:"stdout"` 19 | Stderr []string `json:"stderr" yaml:"stderr"` 20 | Timeout int `json:"timeout" yaml:"timeout"` 21 | } 22 | 23 | func (c *Command) ID() string { return c.Command } 24 | func (c *Command) SetID(id string) { c.Command = id } 25 | 26 | func (c *Command) GetTitle() string { return c.Title } 27 | func (c *Command) GetMeta() meta { return c.Meta } 28 | 29 | func (c *Command) Validate(sys *system.System) []TestResult { 30 | skip := false 31 | if c.Timeout == 0 { 32 | c.Timeout = 10000 33 | } 34 | sysCommand := sys.NewCommand(c.Command, sys, util.Config{Timeout: c.Timeout}) 35 | 36 | var results []TestResult 37 | cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.Command)) 38 | results = append(results, ValidateValue(c, "exit-status", cExitStatus, sysCommand.ExitStatus, skip)) 39 | if len(c.Stdout) > 0 { 40 | results = append(results, ValidateContains(c, "stdout", c.Stdout, sysCommand.Stdout, skip)) 41 | } 42 | if len(c.Stderr) > 0 { 43 | results = append(results, ValidateContains(c, "stderr", c.Stderr, sysCommand.Stderr, skip)) 44 | } 45 | return results 46 | } 47 | 48 | func NewCommand(sysCommand system.Command, config util.Config) (*Command, error) { 49 | command := sysCommand.Command() 50 | exitStatus, err := sysCommand.ExitStatus() 51 | c := &Command{ 52 | Command: command, 53 | ExitStatus: exitStatus, 54 | Stdout: []string{}, 55 | Stderr: []string{}, 56 | Timeout: config.Timeout, 57 | } 58 | 59 | if !contains(config.IgnoreList, "stdout") { 60 | stdout, _ := sysCommand.Stdout() 61 | c.Stdout = readerToSlice(stdout) 62 | } 63 | if !contains(config.IgnoreList, "stderr") { 64 | stderr, _ := sysCommand.Stderr() 65 | c.Stderr = readerToSlice(stderr) 66 | } 67 | 68 | return c, err 69 | } 70 | 71 | func escapePattern(s string) string { 72 | if strings.HasPrefix(s, "!") || strings.HasPrefix(s, "/") { 73 | return "\\" + s 74 | } 75 | return s 76 | } 77 | 78 | func readerToSlice(reader io.Reader) []string { 79 | scanner := bufio.NewScanner(reader) 80 | slice := []string{} 81 | for scanner.Scan() { 82 | line := strings.TrimSpace(scanner.Text()) 83 | line = escapePattern(line) 84 | if line != "" { 85 | slice = append(slice, line) 86 | } 87 | } 88 | 89 | return slice 90 | } 91 | -------------------------------------------------------------------------------- /integration-tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeu 4 | 5 | os=$1 6 | arch=$2 7 | 8 | seccomp_opts() { 9 | local docker_ver minor_ver 10 | docker_ver=$(docker version -f '{{.Client.Version}}') 11 | minor_ver=$(cut -d'.' -f2 <<<$docker_ver) 12 | if ((minor_ver>=10)); then 13 | echo '--security-opt seccomp:unconfined' 14 | fi 15 | } 16 | 17 | cp "../release/goss-linux-$arch" "goss/$os/" 18 | # Run build if Dockerfile has changed but hasn't been pushed to dockerhub 19 | if ! md5sum -c "Dockerfile_${os}.md5"; then 20 | docker build -t "aelsabbahy/goss_${os}:latest" - < "Dockerfile_$os" 21 | # Pull if image doesn't exist locally 22 | elif ! docker images | grep "aelsabbahy/goss_$os";then 23 | docker pull "aelsabbahy/goss_$os" 24 | fi 25 | 26 | container_name="goss_int_test_${os}_${arch}" 27 | docker_exec() { 28 | docker exec "$container_name" "$@" 29 | } 30 | 31 | # Cleanup any old containers 32 | if docker ps -a | grep "$container_name";then 33 | docker rm -vf "$container_name" 34 | fi 35 | opts=(--env OS=$os --cap-add SYS_ADMIN -v "$PWD/goss:/goss" -d --name "$container_name" $(seccomp_opts)) 36 | id=$(docker run "${opts[@]}" "aelsabbahy/goss_$os" /sbin/init) 37 | ip=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$id") 38 | trap "rv=\$?; docker rm -vf $id; exit \$rv" INT TERM EXIT 39 | # Give httpd time to start up, adding 1 second to see if it helps with intermittent CI failures 40 | [[ $os != "arch" ]] && docker_exec "/goss/$os/goss-linux-$arch" -g "/goss/goss-wait.yaml" validate -r 10s -s 100ms && sleep 1 41 | 42 | #out=$(docker exec "$container_name" bash -c "time /goss/$os/goss-linux-$arch -g /goss/$os/goss.yaml validate") 43 | out=$(docker_exec "/goss/$os/goss-linux-$arch" --vars "/goss/vars.yaml" -g "/goss/$os/goss.yaml" validate) 44 | echo "$out" 45 | 46 | if [[ $os == "arch" ]]; then 47 | egrep -q 'Count: 65, Failed: 0' <<<"$out" 48 | else 49 | egrep -q 'Count: 79, Failed: 0' <<<"$out" 50 | fi 51 | 52 | if [[ ! $os == "arch" ]]; then 53 | docker_exec /goss/generate_goss.sh "$os" "$arch" 54 | 55 | #docker exec $container_name bash -c "cp /goss/${os}/goss-generated-$arch.yaml /goss/${os}/goss-expected.yaml" 56 | docker_exec diff -wu "/goss/${os}/goss-expected.yaml" "/goss/${os}/goss-generated-$arch.yaml" 57 | 58 | #docker exec $container_name bash -c "cp /goss/${os}/goss-aa-generated-$arch.yaml /goss/${os}/goss-aa-expected.yaml" 59 | docker_exec diff -wu "/goss/${os}/goss-aa-expected.yaml" "/goss/${os}/goss-aa-generated-$arch.yaml" 60 | 61 | docker_exec /goss/generate_goss.sh "$os" "$arch" -q 62 | 63 | #docker exec $container_name bash -c "cp /goss/${os}/goss-generated-$arch.yaml /goss/${os}/goss-expected-q.yaml" 64 | docker_exec diff -wu "/goss/${os}/goss-expected-q.yaml" "/goss/${os}/goss-generated-$arch.yaml" 65 | fi 66 | 67 | #docker rm -vf goss_int_test_$os 68 | -------------------------------------------------------------------------------- /system/user.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/aelsabbahy/goss/util" 8 | "github.com/opencontainers/runc/libcontainer/user" 9 | ) 10 | 11 | type User interface { 12 | Username() string 13 | Exists() (bool, error) 14 | UID() (int, error) 15 | GID() (int, error) 16 | Groups() ([]string, error) 17 | Home() (string, error) 18 | Shell() (string, error) 19 | } 20 | 21 | type DefUser struct { 22 | username string 23 | } 24 | 25 | func NewDefUser(username string, system *System, config util.Config) User { 26 | return &DefUser{username: username} 27 | } 28 | 29 | func (u *DefUser) Username() string { 30 | return u.username 31 | } 32 | 33 | func (u *DefUser) Exists() (bool, error) { 34 | _, err := user.LookupUser(u.username) 35 | if err != nil { 36 | return false, nil 37 | } 38 | return true, nil 39 | } 40 | 41 | func (u *DefUser) UID() (int, error) { 42 | user, err := user.LookupUser(u.username) 43 | if err != nil { 44 | return 0, err 45 | } 46 | 47 | return user.Uid, nil 48 | } 49 | 50 | func (u *DefUser) GID() (int, error) { 51 | user, err := user.LookupUser(u.username) 52 | if err != nil { 53 | return 0, err 54 | } 55 | 56 | return user.Gid, nil 57 | } 58 | 59 | func (u *DefUser) Home() (string, error) { 60 | user, err := user.LookupUser(u.username) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | return user.Home, nil 66 | } 67 | 68 | func (u *DefUser) Shell() (string, error) { 69 | user, err := user.LookupUser(u.username) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | return user.Shell, nil 75 | } 76 | 77 | func (u *DefUser) Groups() ([]string, error) { 78 | user, err := user.LookupUser(u.username) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | var groupList []string 84 | groups, err := lookupUserGroups(user) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | for _, g := range groups { 90 | groupList = append(groupList, g.Name) 91 | } 92 | 93 | sort.Strings(groupList) 94 | return groupList, nil 95 | } 96 | 97 | func lookupUserGroups(userS user.User) ([]user.Group, error) { 98 | // Get operating system-specific group reader-closer. 99 | group, err := user.GetGroup() 100 | if err != nil { 101 | return []user.Group{user.Group{}}, err 102 | } 103 | defer group.Close() 104 | 105 | groups, err := user.ParseGroupFilter(group, func(g user.Group) bool { 106 | // Primary group 107 | if g.Gid == userS.Gid { 108 | return true 109 | } 110 | 111 | // Check if user is a member. 112 | for _, u := range g.List { 113 | if u == userS.Name { 114 | return true 115 | } 116 | } 117 | 118 | return false 119 | }) 120 | 121 | if err != nil { 122 | return []user.Group{user.Group{}}, fmt.Errorf("Unable to find groups for user %v: %v", userS, err) 123 | } 124 | 125 | return groups, nil 126 | } 127 | -------------------------------------------------------------------------------- /integration-tests/goss/generate_goss.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR=$(readlink -f $(dirname $0)) 4 | 5 | OS=$1 6 | ARCH=$2 7 | [[ $3 == "-q" ]] && args=("--exclude-attr" "*") 8 | 9 | goss() { 10 | $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml "$@" 11 | # Validate that duplicates are ignored 12 | $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml "$@" 13 | } 14 | 15 | rm -f $SCRIPT_DIR/${OS}/goss*generated*-$ARCH.yaml 16 | 17 | for x in /etc/passwd /tmp/goss/foobar;do 18 | goss a "${args[@]}" file $x 19 | done 20 | 21 | [[ $OS == "centos7" ]] && package="httpd" || package="apache2" 22 | [[ $OS == "centos7" ]] && user="apache" || user="www-data" 23 | goss a "${args[@]}" package $package foobar vim-tiny 24 | 25 | goss a "${args[@]}" addr --timeout 1s google.com:443 google.com:22 26 | 27 | goss a "${args[@]}" port tcp:80 tcp6:80 9999 28 | 29 | goss a "${args[@]}" service $package foobar 30 | 31 | goss a "${args[@]}" user $user foobar 32 | 33 | goss a "${args[@]}" group $user foobar 34 | 35 | goss a "${args[@]}" command "echo 'hi'" foobar 36 | 37 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 CNAME:c.dnstest.io 38 | 39 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 MX:dnstest.io 40 | 41 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 NS:dnstest.io 42 | 43 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 PTR:8.8.8.8 44 | 45 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 SRV:_https._tcp.dnstest.io 46 | 47 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 TXT:txt._test.dnstest.io 48 | 49 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 CAA:dnstest.io 50 | 51 | goss a "${args[@]}" dns --timeout 1s --server 8.8.8.8 ip6.dnstest.io 52 | 53 | goss a "${args[@]}" dns --timeout 1s localhost 54 | 55 | goss a "${args[@]}" process $package foobar 56 | 57 | goss a "${args[@]}" kernel-param kernel.ostype 58 | 59 | goss a "${args[@]}" mount /dev 60 | 61 | goss a "${args[@]}" http https://www.google.com 62 | 63 | goss a "${args[@]}" http http://google.com -r 64 | 65 | # Auto-add 66 | # Validate that empty configs don't get created 67 | $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa nosuchresource 68 | if [[ -f $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml ]] 69 | then 70 | echo "Error! Empty config file exists!" && exit 1 71 | fi 72 | $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa $package 73 | # Validate that duplicates are ignored 74 | $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa $package 75 | # Validate that we can aa none existent resources without destroying the file 76 | $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa nosuchresource 77 | if [[ ! -f $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml ]] 78 | then 79 | echo "Error! Config file removed by aa!" && exit 1 80 | fi 81 | -------------------------------------------------------------------------------- /resource/user.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aelsabbahy/goss/system" 7 | "github.com/aelsabbahy/goss/util" 8 | ) 9 | 10 | type User struct { 11 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 12 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 13 | Username string `json:"-" yaml:"-"` 14 | Exists matcher `json:"exists" yaml:"exists"` 15 | UID matcher `json:"uid,omitempty" yaml:"uid,omitempty"` 16 | GID matcher `json:"gid,omitempty" yaml:"gid,omitempty"` 17 | Groups matcher `json:"groups,omitempty" yaml:"groups,omitempty"` 18 | Home matcher `json:"home,omitempty" yaml:"home,omitempty"` 19 | Shell matcher `json:"shell,omitempty" yaml:"shell,omitempty"` 20 | } 21 | 22 | func (u *User) ID() string { return u.Username } 23 | func (u *User) SetID(id string) { u.Username = id } 24 | 25 | func (u *User) GetTitle() string { return u.Title } 26 | func (u *User) GetMeta() meta { return u.Meta } 27 | 28 | func (u *User) Validate(sys *system.System) []TestResult { 29 | skip := false 30 | sysuser := sys.NewUser(u.Username, sys, util.Config{}) 31 | 32 | var results []TestResult 33 | results = append(results, ValidateValue(u, "exists", u.Exists, sysuser.Exists, skip)) 34 | if shouldSkip(results) { 35 | skip = true 36 | } 37 | if u.UID != nil { 38 | uUID := deprecateAtoI(u.UID, fmt.Sprintf("%s: user.uid", u.Username)) 39 | results = append(results, ValidateValue(u, "uid", uUID, sysuser.UID, skip)) 40 | } 41 | if u.GID != nil { 42 | uGID := deprecateAtoI(u.GID, fmt.Sprintf("%s: user.gid", u.Username)) 43 | results = append(results, ValidateValue(u, "gid", uGID, sysuser.GID, skip)) 44 | } 45 | if u.Home != nil { 46 | results = append(results, ValidateValue(u, "home", u.Home, sysuser.Home, skip)) 47 | } 48 | if u.Groups != nil { 49 | results = append(results, ValidateValue(u, "groups", u.Groups, sysuser.Groups, skip)) 50 | } 51 | if u.Shell != nil { 52 | results = append(results, ValidateValue(u, "shell", u.Shell, sysuser.Shell, skip)) 53 | } 54 | return results 55 | } 56 | 57 | func NewUser(sysUser system.User, config util.Config) (*User, error) { 58 | username := sysUser.Username() 59 | exists, _ := sysUser.Exists() 60 | u := &User{ 61 | Username: username, 62 | Exists: exists, 63 | } 64 | if !contains(config.IgnoreList, "uid") { 65 | if uid, err := sysUser.UID(); err == nil { 66 | u.UID = uid 67 | } 68 | } 69 | if !contains(config.IgnoreList, "gid") { 70 | if gid, err := sysUser.GID(); err == nil { 71 | u.GID = gid 72 | } 73 | } 74 | if !contains(config.IgnoreList, "groups") { 75 | if groups, err := sysUser.Groups(); err == nil { 76 | u.Groups = groups 77 | } 78 | } 79 | if !contains(config.IgnoreList, "home") { 80 | if home, err := sysUser.Home(); err == nil { 81 | u.Home = home 82 | } 83 | } 84 | if !contains(config.IgnoreList, "shell") { 85 | if shell, err := sysUser.Shell(); err == nil { 86 | u.Shell = shell 87 | } 88 | } 89 | return u, nil 90 | } 91 | -------------------------------------------------------------------------------- /resource/matching.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/aelsabbahy/goss/system" 10 | "github.com/aelsabbahy/goss/util" 11 | ) 12 | 13 | type Matching struct { 14 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 15 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 16 | Content interface{} `json:"content,omitempty" yaml:"content,omitempty"` 17 | Id string `json:"-" yaml:"-"` 18 | Matches matcher `json:"matches" yaml:"matches"` 19 | } 20 | 21 | type MatchingMap map[string]*Matching 22 | 23 | func (a *Matching) ID() string { return a.Id } 24 | func (a *Matching) SetID(id string) { a.Id = id } 25 | 26 | // FIXME: Can this be refactored? 27 | func (r *Matching) GetTitle() string { return r.Title } 28 | func (r *Matching) GetMeta() meta { return r.Meta } 29 | 30 | func (a *Matching) Validate(sys system.System) []TestResult { 31 | skip := false 32 | 33 | // ValidateValue expects a function 34 | stub := func() (interface{}, error) { 35 | return a.Content, nil 36 | } 37 | 38 | var results []TestResult 39 | results = append(results, ValidateValue(a, "matches", a.Matches, stub, skip)) 40 | return results 41 | } 42 | 43 | func (ret *MatchingMap) UnmarshalJSON(data []byte) error { 44 | // Curried json.Unmarshal 45 | unmarshal := func(i interface{}) error { 46 | if err := json.Unmarshal(data, i); err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | // Validate configuration 53 | zero := Matching{} 54 | whitelist, err := util.WhitelistAttrs(zero, util.JSON) 55 | if err != nil { 56 | return err 57 | } 58 | if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { 59 | return err 60 | } 61 | 62 | var tmp map[string]*Matching 63 | if err := unmarshal(&tmp); err != nil { 64 | return err 65 | } 66 | 67 | typ := reflect.TypeOf(zero) 68 | typs := strings.Split(typ.String(), ".")[1] 69 | for id, res := range tmp { 70 | if res == nil { 71 | return fmt.Errorf("Could not parse resource %s:%s", typs, id) 72 | } 73 | res.SetID(id) 74 | } 75 | 76 | *ret = tmp 77 | return nil 78 | } 79 | 80 | func (ret *MatchingMap) UnmarshalYAML(unmarshal func(v interface{}) error) error { 81 | // Validate configuration 82 | zero := Matching{} 83 | whitelist, err := util.WhitelistAttrs(zero, util.YAML) 84 | if err != nil { 85 | return err 86 | } 87 | if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { 88 | return err 89 | } 90 | 91 | var tmp map[string]*Matching 92 | if err := unmarshal(&tmp); err != nil { 93 | return err 94 | } 95 | 96 | typ := reflect.TypeOf(zero) 97 | typs := strings.Split(typ.String(), ".")[1] 98 | for id, res := range tmp { 99 | if res == nil { 100 | return fmt.Errorf("Could not parse resource %s:%s", typs, id) 101 | } 102 | res.SetID(id) 103 | } 104 | 105 | *ret = tmp 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /system/port.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/aelsabbahy/GOnetstat" 8 | "github.com/aelsabbahy/goss/util" 9 | ) 10 | 11 | type Port interface { 12 | Port() string 13 | Exists() (bool, error) 14 | Listening() (bool, error) 15 | IP() ([]string, error) 16 | } 17 | 18 | type DefPort struct { 19 | port string 20 | sysPorts map[string][]GOnetstat.Process 21 | } 22 | 23 | func NewDefPort(port string, system *System, config util.Config) Port { 24 | p := normalizePort(port) 25 | return &DefPort{ 26 | port: p, 27 | sysPorts: system.Ports(), 28 | } 29 | } 30 | 31 | func splitPort(fullport string) (network, port string) { 32 | split := strings.SplitN(fullport, ":", 2) 33 | if len(split) == 2 { 34 | return split[0], split[1] 35 | } 36 | return "tcp", fullport 37 | 38 | } 39 | 40 | func normalizePort(fullport string) string { 41 | net, addr := splitPort(fullport) 42 | return net + ":" + addr 43 | } 44 | 45 | func (p *DefPort) Port() string { 46 | return p.port 47 | } 48 | 49 | func (p *DefPort) Exists() (bool, error) { return p.Listening() } 50 | 51 | func (p *DefPort) Listening() (bool, error) { 52 | if _, ok := p.sysPorts[p.port]; ok { 53 | return true, nil 54 | } 55 | return false, nil 56 | } 57 | 58 | func (p *DefPort) IP() ([]string, error) { 59 | var ips []string 60 | for _, entry := range p.sysPorts[p.port] { 61 | ips = append(ips, entry.Ip) 62 | } 63 | return ips, nil 64 | } 65 | 66 | // FIXME: Is there a better way to do this rather than ignoring errors? 67 | func GetPorts(lookupPids bool) map[string][]GOnetstat.Process { 68 | ports := make(map[string][]GOnetstat.Process) 69 | netstat, _ := GOnetstat.Tcp(lookupPids) 70 | var net string 71 | //netPorts := make(map[string]GOnetstat.Process) 72 | //ports["tcp"] = netPorts 73 | net = "tcp" 74 | for _, entry := range netstat { 75 | if entry.State == "LISTEN" { 76 | port := strconv.FormatInt(entry.Port, 10) 77 | ports[net+":"+port] = append(ports[net+":"+port], entry) 78 | } 79 | } 80 | netstat, _ = GOnetstat.Tcp6(lookupPids) 81 | //netPorts = make(map[string]GOnetstat.Process) 82 | //ports["tcp6"] = netPorts 83 | net = "tcp6" 84 | for _, entry := range netstat { 85 | if entry.State == "LISTEN" { 86 | port := strconv.FormatInt(entry.Port, 10) 87 | ports[net+":"+port] = append(ports[net+":"+port], entry) 88 | } 89 | } 90 | netstat, _ = GOnetstat.Udp(lookupPids) 91 | //netPorts = make(map[string]GOnetstat.Process) 92 | //ports["udp"] = netPorts 93 | net = "udp" 94 | for _, entry := range netstat { 95 | port := strconv.FormatInt(entry.Port, 10) 96 | ports[net+":"+port] = append(ports[net+":"+port], entry) 97 | } 98 | netstat, _ = GOnetstat.Udp6(lookupPids) 99 | //netPorts = make(map[string]GOnetstat.Process) 100 | //ports["udp6"] = netPorts 101 | net = "udp6" 102 | for _, entry := range netstat { 103 | port := strconv.FormatInt(entry.Port, 10) 104 | ports[net+":"+port] = append(ports[net+":"+port], entry) 105 | } 106 | return ports 107 | } 108 | -------------------------------------------------------------------------------- /development/debian/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure(2) do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://atlas.hashicorp.com/search. 15 | config.vm.box = "debian/jessie64" 16 | 17 | # Disable automatic box update checking. If you disable this, then 18 | # boxes will only be checked for updates when the user runs 19 | # `vagrant box outdated`. This is not recommended. 20 | # config.vm.box_check_update = false 21 | 22 | # Create a forwarded port mapping which allows access to a specific port 23 | # within the machine from a port on the host machine. In the example below, 24 | # accessing "localhost:8080" will access port 80 on the guest machine. 25 | # config.vm.network "forwarded_port", guest: 80, host: 8080 26 | 27 | # Create a private network, which allows host-only access to the machine 28 | # using a specific IP. 29 | # config.vm.network "private_network", ip: "192.168.33.10" 30 | 31 | # Create a public network, which generally matched to bridged network. 32 | # Bridged networks make the machine appear as another physical device on 33 | # your network. 34 | # config.vm.network "public_network" 35 | 36 | # Share an additional folder to the guest VM. The first argument is 37 | # the path on the host to the actual folder. The second argument is 38 | # the path on the guest to mount the folder. And the optional third 39 | # argument is a set of non-required options. 40 | # config.vm.synced_folder "../data", "/vagrant_data" 41 | 42 | # Provider-specific configuration so you can fine-tune various 43 | # backing providers for Vagrant. These expose provider-specific options. 44 | # Example for VirtualBox: 45 | # 46 | # config.vm.provider "virtualbox" do |vb| 47 | # # Display the VirtualBox GUI when booting the machine 48 | # vb.gui = true 49 | # 50 | # # Customize the amount of memory on the VM: 51 | # vb.memory = "1024" 52 | # end 53 | # 54 | # View the documentation for the provider you are using for more 55 | # information on available options. 56 | 57 | # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies 58 | # such as FTP and Heroku are also available. See the documentation at 59 | # https://docs.vagrantup.com/v2/push/atlas.html for more information. 60 | # config.push.define "atlas" do |push| 61 | # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" 62 | # end 63 | 64 | # Enable provisioning with a shell script. Additional provisioners such as 65 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 66 | # documentation for more information about their specific syntax and use. 67 | # config.vm.provision "shell", inline: <<-SHELL 68 | # sudo apt-get update 69 | # sudo apt-get install -y apache2 70 | # SHELL 71 | end 72 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "sync" 10 | "time" 11 | 12 | "github.com/aelsabbahy/goss/outputs" 13 | "github.com/aelsabbahy/goss/resource" 14 | "github.com/aelsabbahy/goss/system" 15 | "github.com/fatih/color" 16 | "github.com/urfave/cli" 17 | ) 18 | 19 | func getGossConfig(c *cli.Context) GossConfig { 20 | // handle stdin 21 | var fh *os.File 22 | var path, source string 23 | var gossConfig GossConfig 24 | TemplateFilter = NewTemplateFilter(c.GlobalString("vars")) 25 | specFile := c.GlobalString("gossfile") 26 | if specFile == "-" { 27 | source = "STDIN" 28 | fh = os.Stdin 29 | data, err := ioutil.ReadAll(fh) 30 | if err != nil { 31 | fmt.Printf("Error: %v\n", err) 32 | os.Exit(1) 33 | } 34 | OutStoreFormat = getStoreFormatFromData(data) 35 | gossConfig = ReadJSONData(data, true) 36 | } else { 37 | source = specFile 38 | path = filepath.Dir(specFile) 39 | OutStoreFormat = getStoreFormatFromFileName(specFile) 40 | gossConfig = ReadJSON(specFile) 41 | } 42 | 43 | gossConfig = mergeJSONData(gossConfig, 0, path) 44 | 45 | if len(gossConfig.Resources()) == 0 { 46 | fmt.Printf("Error: found 0 tests, source: %v\n", source) 47 | os.Exit(1) 48 | } 49 | return gossConfig 50 | } 51 | 52 | func getOutputer(c *cli.Context) outputs.Outputer { 53 | if c.Bool("no-color") { 54 | color.NoColor = true 55 | } 56 | if c.Bool("color") { 57 | color.NoColor = false 58 | } 59 | return outputs.GetOutputer(c.String("format")) 60 | } 61 | 62 | func Validate(c *cli.Context, startTime time.Time) { 63 | gossConfig := getGossConfig(c) 64 | sys := system.New(c) 65 | outputer := getOutputer(c) 66 | 67 | sleep := c.Duration("sleep") 68 | retryTimeout := c.Duration("retry-timeout") 69 | i := 1 70 | for { 71 | iStartTime := time.Now() 72 | out := validate(sys, gossConfig, c.Int("max-concurrent")) 73 | exitCode := outputer.Output(os.Stdout, out, iStartTime) 74 | if retryTimeout == 0 || exitCode == 0 { 75 | os.Exit(exitCode) 76 | } 77 | elapsed := time.Since(startTime) 78 | if elapsed+sleep > retryTimeout { 79 | color.Red("\nERROR: Timeout of %s reached before tests entered a passing state", retryTimeout) 80 | os.Exit(3) 81 | } 82 | color.Red("Retrying in %s (elapsed/timeout time: %.3fs/%s)\n\n\n", sleep, elapsed.Seconds(), retryTimeout) 83 | // Reset cache 84 | sys = system.New(c) 85 | time.Sleep(sleep) 86 | i++ 87 | fmt.Printf("Attempt #%d:\n", i) 88 | } 89 | } 90 | 91 | func validate(sys *system.System, gossConfig GossConfig, maxConcurrent int) <-chan []resource.TestResult { 92 | out := make(chan []resource.TestResult) 93 | in := make(chan resource.Resource) 94 | 95 | go func() { 96 | for _, t := range gossConfig.Resources() { 97 | in <- t 98 | } 99 | close(in) 100 | }() 101 | 102 | workerCount := runtime.NumCPU() * 5 103 | if workerCount > maxConcurrent { 104 | workerCount = maxConcurrent 105 | } 106 | var wg sync.WaitGroup 107 | for i := 0; i < workerCount; i++ { 108 | wg.Add(1) 109 | go func() { 110 | defer wg.Done() 111 | for f := range in { 112 | out <- f.Validate(sys) 113 | } 114 | 115 | }() 116 | } 117 | 118 | go func() { 119 | wg.Wait() 120 | close(out) 121 | }() 122 | 123 | return out 124 | } 125 | -------------------------------------------------------------------------------- /extras/dgoss/dgoss: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | USAGE="USAGE: $(basename "$0") [run|edit] " 6 | GOSS_FILES_PATH="${GOSS_FILES_PATH:-.}" 7 | 8 | info() { echo -e "INFO: $*"; } 9 | error() { echo -e "ERROR: $*";exit 1; } 10 | 11 | cleanup() { 12 | set +e 13 | { kill "$log_pid" && wait "$log_pid"; } 2> /dev/null 14 | rm -rf "$tmp_dir" 15 | if [[ $id ]];then 16 | info "Deleting container" 17 | docker rm -vf "$id" > /dev/null 18 | fi 19 | } 20 | 21 | run(){ 22 | # Copy in goss 23 | cp "${GOSS_PATH}" "$tmp_dir/goss" 24 | chmod 755 "$tmp_dir/goss" 25 | [[ -e "${GOSS_FILES_PATH}/goss.yaml" ]] && cp "${GOSS_FILES_PATH}/goss.yaml" "$tmp_dir" 26 | [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]] && cp "${GOSS_FILES_PATH}/goss_wait.yaml" "$tmp_dir" 27 | [[ ! -z "${GOSS_VARS}" ]] && [[ -e "${GOSS_FILES_PATH}/${GOSS_VARS}" ]] && cp "${GOSS_FILES_PATH}/${GOSS_VARS}" "$tmp_dir" 28 | info "Starting docker container" 29 | id=$(docker run -d -v "$tmp_dir:/goss" "${@:2}") 30 | docker logs -f "$id" > "$tmp_dir/docker_output.log" 2>&1 & 31 | log_pid=$! 32 | info "Container ID: ${id:0:8}" 33 | } 34 | 35 | get_docker_file() { 36 | if docker exec "$id" sh -c "test -e $1" > /dev/null;then 37 | mkdir -p "${GOSS_FILES_PATH}" 38 | info "Copied '$1' from container to '${GOSS_FILES_PATH}'" 39 | docker cp "$id:$1" "${GOSS_FILES_PATH}" 40 | fi 41 | } 42 | 43 | # Main 44 | tmp_dir=$(mktemp -d /tmp/tmp.XXXXXXXXXX) 45 | chmod 777 "$tmp_dir" 46 | trap 'ret=$?;cleanup;exit $ret' EXIT 47 | 48 | GOSS_PATH="${GOSS_PATH:-$(which goss 2> /dev/null || true)}" 49 | [[ $GOSS_PATH ]] || { error "Couldn't find goss installation, please set GOSS_PATH to it"; } 50 | [[ ${GOSS_OPTS+x} ]] || GOSS_OPTS="--color --format documentation" 51 | [[ ${GOSS_WAIT_OPTS+x} ]] || GOSS_WAIT_OPTS="-r 30s -s 1s > /dev/null" 52 | GOSS_SLEEP=${GOSS_SLEEP:-0.2} 53 | 54 | case "$1" in 55 | run) 56 | run "$@" 57 | if [[ -e "${GOSS_FILES_PATH}/goss_wait.yaml" ]]; then 58 | info "Found goss_wait.yaml, waiting for it to pass before running tests" 59 | if [[ -z "${GOSS_VARS}" ]]; then 60 | if ! docker exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml validate $GOSS_WAIT_OPTS"; then 61 | error "goss_wait.yaml never passed" 62 | fi 63 | else 64 | if ! docker exec "$id" sh -c "/goss/goss -g /goss/goss_wait.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_WAIT_OPTS"; then 65 | error "goss_wait.yaml never passed" 66 | fi 67 | fi 68 | fi 69 | [[ $GOSS_SLEEP ]] && { info "Sleeping for $GOSS_SLEEP"; sleep "$GOSS_SLEEP"; } 70 | info "Running Tests" 71 | if [[ -z "${GOSS_VARS}" ]]; then 72 | docker exec "$id" sh -c "/goss/goss -g /goss/goss.yaml validate $GOSS_OPTS" 73 | else 74 | docker exec "$id" sh -c "/goss/goss -g /goss/goss.yaml --vars='/goss/${GOSS_VARS}' validate $GOSS_OPTS" 75 | fi 76 | ;; 77 | edit) 78 | run "$@" 79 | info "Run goss add/autoadd to add resources" 80 | docker exec -it "$id" sh -c 'cd /goss; PATH="/goss:$PATH" exec sh' 81 | get_docker_file "/goss/goss.yaml" 82 | get_docker_file "/goss/goss_wait.yaml" 83 | [[ ! -z "${GOSS_VARS}" ]] && get_docker_file "/goss/${GOSS_VARS}" 84 | ;; 85 | *) 86 | error "$USAGE" 87 | esac 88 | -------------------------------------------------------------------------------- /resource/resource_list_genny.go: -------------------------------------------------------------------------------- 1 | // +build genny 2 | 3 | package resource 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/aelsabbahy/goss/system" 12 | "github.com/aelsabbahy/goss/util" 13 | "github.com/cheekybits/genny/generic" 14 | ) 15 | 16 | //go:generate genny -in=$GOFILE -out=resource_list.go gen "ResourceType=Addr,Command,DNS,File,Gossfile,Group,Package,Port,Process,Service,User,KernelParam,Mount,Interface,HTTP" 17 | //go:generate sed -i -e "/^\\/\\/ +build genny/d" resource_list.go 18 | //go:generate goimports -w resource_list.go resource_list.go 19 | 20 | type ResourceType generic.Type 21 | 22 | type ResourceTypeMap map[string]*ResourceType 23 | 24 | func (r ResourceTypeMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*ResourceType, error) { 25 | sysres := sys.NewResourceType(sr, sys, config) 26 | res, err := NewResourceType(sysres, config) 27 | if err != nil { 28 | return nil, err 29 | } 30 | if old_res, ok := r[res.ID()]; ok { 31 | res.Title = old_res.Title 32 | res.Meta = old_res.Meta 33 | } 34 | r[res.ID()] = res 35 | return res, nil 36 | } 37 | 38 | func (r ResourceTypeMap) AppendSysResourceIfExists(sr string, sys *system.System) (*ResourceType, system.ResourceType, bool) { 39 | sysres := sys.NewResourceType(sr, sys, util.Config{}) 40 | // FIXME: Do we want to be silent about errors? 41 | res, _ := NewResourceType(sysres, util.Config{}) 42 | if e, _ := sysres.Exists(); e != true { 43 | return res, sysres, false 44 | } 45 | if old_res, ok := r[res.ID()]; ok { 46 | res.Title = old_res.Title 47 | res.Meta = old_res.Meta 48 | } 49 | r[res.ID()] = res 50 | return res, sysres, true 51 | } 52 | 53 | func (ret *ResourceTypeMap) UnmarshalJSON(data []byte) error { 54 | // Curried json.Unmarshal 55 | unmarshal := func(i interface{}) error { 56 | if err := json.Unmarshal(data, i); err != nil { 57 | return err 58 | } 59 | return nil 60 | } 61 | 62 | // Validate configuration 63 | zero := ResourceType{} 64 | whitelist, err := util.WhitelistAttrs(zero, util.JSON) 65 | if err != nil { 66 | return err 67 | } 68 | if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { 69 | return err 70 | } 71 | 72 | var tmp map[string]*ResourceType 73 | if err := unmarshal(&tmp); err != nil { 74 | return err 75 | } 76 | 77 | typ := reflect.TypeOf(zero) 78 | typs := strings.Split(typ.String(), ".")[1] 79 | for id, res := range tmp { 80 | if res == nil { 81 | return fmt.Errorf("Could not parse resource %s:%s", typs, id) 82 | } 83 | res.SetID(id) 84 | } 85 | 86 | *ret = tmp 87 | return nil 88 | } 89 | 90 | func (ret *ResourceTypeMap) UnmarshalYAML(unmarshal func(v interface{}) error) error { 91 | // Validate configuration 92 | zero := ResourceType{} 93 | whitelist, err := util.WhitelistAttrs(zero, util.YAML) 94 | if err != nil { 95 | return err 96 | } 97 | if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { 98 | return err 99 | } 100 | 101 | var tmp map[string]*ResourceType 102 | if err := unmarshal(&tmp); err != nil { 103 | return err 104 | } 105 | 106 | typ := reflect.TypeOf(zero) 107 | typs := strings.Split(typ.String(), ".")[1] 108 | for id, res := range tmp { 109 | if res == nil { 110 | return fmt.Errorf("Could not parse resource %s:%s", typs, id) 111 | } 112 | res.SetID(id) 113 | } 114 | 115 | *ret = tmp 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GO15VENDOREXPERIMENT=1 2 | 3 | exe = github.com/aelsabbahy/goss/cmd/goss 4 | pkgs = $(shell glide novendor) 5 | cmd = goss 6 | TRAVIS_TAG ?= "0.0.0" 7 | GO_FILES = $(shell find . \( -path ./vendor -o -name '_test.go' \) -prune -o -name '*.go' -print) 8 | 9 | .PHONY: all build install test coverage deps release bench test-int lint gen centos7 wheezy precise alpine3 arch test-int32 centos7-32 wheezy-32 precise-32 alpine3-32 arch-32 10 | 11 | all: test-all test-all-32 12 | 13 | install: release/goss-linux-amd64 14 | $(info INFO: Starting build $@) 15 | cp release/$(cmd)-linux-amd64 $(GOPATH)/bin/goss 16 | 17 | test: 18 | $(info INFO: Starting build $@) 19 | go test $(pkgs) 20 | 21 | lint: 22 | $(info INFO: Starting build $@) 23 | #go tool vet . 24 | golint $(pkgs) | grep -v 'unexported' || true 25 | 26 | bench: 27 | $(info INFO: Starting build $@) 28 | go test -bench=. 29 | 30 | coverage: 31 | $(info INFO: Starting build $@) 32 | go test -cover $(pkgs) 33 | #go test -coverprofile=/tmp/coverage.out . 34 | #go tool cover -func=/tmp/coverage.out 35 | #go tool cover -html=/tmp/coverage.out -o /tmp/coverage.html 36 | #xdg-open /tmp/coverage.html 37 | 38 | release/goss-linux-386: $(GO_FILES) 39 | $(info INFO: Starting build $@) 40 | CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags "-X main.version=$(TRAVIS_TAG) -s -w" -o release/$(cmd)-linux-386 $(exe) 41 | release/goss-linux-amd64: $(GO_FILES) 42 | $(info INFO: Starting build $@) 43 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=$(TRAVIS_TAG) -s -w" -o release/$(cmd)-linux-amd64 $(exe) 44 | 45 | release: 46 | $(MAKE) clean 47 | $(MAKE) build 48 | 49 | build: release/goss-linux-386 release/goss-linux-amd64 50 | 51 | test-int: centos7 wheezy precise alpine3 arch 52 | test-int-32: centos7-32 wheezy-32 precise-32 alpine3-32 arch-32 53 | 54 | centos7-32: build 55 | $(info INFO: Starting build $@) 56 | cd integration-tests/ && ./test.sh centos7 386 57 | wheezy-32: build 58 | $(info INFO: Starting build $@) 59 | cd integration-tests/ && ./test.sh wheezy 386 60 | precise-32: build 61 | $(info INFO: Starting build $@) 62 | cd integration-tests/ && ./test.sh precise 386 63 | alpine3-32: build 64 | $(info INFO: Starting build $@) 65 | cd integration-tests/ && ./test.sh alpine3 386 66 | arch-32: build 67 | $(info INFO: Starting build $@) 68 | cd integration-tests/ && ./test.sh arch 386 69 | centos7: build 70 | $(info INFO: Starting build $@) 71 | cd integration-tests/ && ./test.sh centos7 amd64 72 | wheezy: build 73 | $(info INFO: Starting build $@) 74 | cd integration-tests/ && ./test.sh wheezy amd64 75 | precise: build 76 | $(info INFO: Starting build $@) 77 | cd integration-tests/ && ./test.sh precise amd64 78 | alpine3: build 79 | $(info INFO: Starting build $@) 80 | cd integration-tests/ && ./test.sh alpine3 amd64 81 | arch: build 82 | $(info INFO: Starting build $@) 83 | cd integration-tests/ && ./test.sh arch amd64 84 | 85 | 86 | test-all-32: lint test test-int-32 87 | test-all: lint test test-int 88 | 89 | deps: 90 | $(info INFO: Starting build $@) 91 | glide install 92 | 93 | gen: 94 | $(info INFO: Starting build $@) 95 | go generate -tags genny $(pkgs) 96 | 97 | clean: 98 | $(info INFO: Starting build $@) 99 | rm -rf ./release 100 | 101 | build-images: 102 | $(info INFO: Starting build $@) 103 | development/build_images.sh 104 | 105 | push-images: 106 | $(info INFO: Starting build $@) 107 | development/push_images.sh 108 | -------------------------------------------------------------------------------- /integration-tests/goss/alpine3/goss-expected.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | mode: "0644" 5 | size: 1325 6 | owner: root 7 | group: root 8 | filetype: file 9 | contains: [] 10 | /tmp/goss/foobar: 11 | exists: false 12 | contains: [] 13 | package: 14 | apache2: 15 | installed: true 16 | versions: 17 | - 2.4.23-r1 18 | foobar: 19 | installed: false 20 | vim-tiny: 21 | installed: false 22 | addr: 23 | tcp://google.com:22: 24 | reachable: false 25 | timeout: 1000 26 | tcp://google.com:443: 27 | reachable: true 28 | timeout: 1000 29 | port: 30 | tcp:80: 31 | listening: false 32 | ip: [] 33 | tcp:9999: 34 | listening: false 35 | ip: [] 36 | tcp6:80: 37 | listening: true 38 | ip: 39 | - '::' 40 | service: 41 | apache2: 42 | enabled: true 43 | running: true 44 | foobar: 45 | enabled: false 46 | running: false 47 | user: 48 | foobar: 49 | exists: false 50 | www-data: 51 | exists: false 52 | group: 53 | foobar: 54 | exists: false 55 | www-data: 56 | exists: true 57 | gid: 82 58 | command: 59 | echo 'hi': 60 | exit-status: 0 61 | stdout: 62 | - hi 63 | stderr: [] 64 | timeout: 10000 65 | foobar: 66 | exit-status: 127 67 | stdout: [] 68 | stderr: 69 | - 'sh: foobar: not found' 70 | timeout: 10000 71 | dns: 72 | CAA:dnstest.io: 73 | resolveable: true 74 | addrs: 75 | - 0 issue comodoca.com 76 | - 0 issue letsencrypt.org 77 | - 0 issuewild ; 78 | timeout: 1000 79 | server: 8.8.8.8 80 | CNAME:c.dnstest.io: 81 | resolveable: true 82 | addrs: 83 | - a.dnstest.io. 84 | timeout: 1000 85 | server: 8.8.8.8 86 | MX:dnstest.io: 87 | resolveable: true 88 | addrs: 89 | - 10 b.dnstest.io. 90 | - 5 a.dnstest.io. 91 | timeout: 1000 92 | server: 8.8.8.8 93 | NS:dnstest.io: 94 | resolveable: true 95 | addrs: 96 | - ns1.dnstest.io. 97 | - ns2.dnstest.io. 98 | timeout: 1000 99 | server: 8.8.8.8 100 | PTR:8.8.8.8: 101 | resolveable: true 102 | addrs: 103 | - google-public-dns-a.google.com. 104 | timeout: 1000 105 | server: 8.8.8.8 106 | SRV:_https._tcp.dnstest.io: 107 | resolveable: true 108 | addrs: 109 | - 0 5 443 a.dnstest.io. 110 | - 10 10 443 b.dnstest.io. 111 | timeout: 1000 112 | server: 8.8.8.8 113 | TXT:txt._test.dnstest.io: 114 | resolveable: true 115 | addrs: 116 | - Hello DNS 117 | timeout: 1000 118 | server: 8.8.8.8 119 | ip6.dnstest.io: 120 | resolveable: true 121 | addrs: 122 | - 2404:6800:4001:807::200e 123 | timeout: 1000 124 | server: 8.8.8.8 125 | localhost: 126 | resolveable: true 127 | addrs: 128 | - 127.0.0.1 129 | - ::1 130 | timeout: 1000 131 | process: 132 | apache2: 133 | running: false 134 | foobar: 135 | running: false 136 | kernel-param: 137 | kernel.ostype: 138 | value: Linux 139 | mount: 140 | /dev: 141 | exists: true 142 | opts: 143 | - rw 144 | - nosuid 145 | source: tmpfs 146 | filesystem: tmpfs 147 | http: 148 | http://google.com: 149 | status: 301 150 | allow-insecure: false 151 | no-follow-redirects: true 152 | timeout: 5000 153 | body: [] 154 | https://www.google.com: 155 | status: 200 156 | allow-insecure: false 157 | no-follow-redirects: false 158 | timeout: 5000 159 | body: [] 160 | -------------------------------------------------------------------------------- /integration-tests/goss/goss-shared.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | command: 3 | echo 'hi': 4 | exit-status: 0 5 | stdout: 6 | - hi 7 | stderr: [] 8 | foobar: 9 | exit-status: 127 10 | stdout: [] 11 | stderr: 12 | - not found 13 | file: 14 | {{range mkSlice "/etc/passwd" "/etc/group"}} 15 | {{.}}: 16 | exists: true 17 | mode: '0644' 18 | owner: root 19 | group: root 20 | filetype: file 21 | contains: 22 | - root 23 | {{end}} 24 | "/goss/hellogoss.txt": 25 | exists: true 26 | md5: 7c9bb14b3bf178e82c00c2a4398c93cd 27 | sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c 28 | "/tmp/goss/foobar": 29 | exists: false 30 | contains: [] 31 | "~root": 32 | exists: true 33 | mode: '0700' 34 | "/tmp": 35 | exists: true 36 | mode: '1777' 37 | package: 38 | foobar: 39 | installed: false 40 | {{- range $name, $ver := index .Vars .Env.OS "packages"}} 41 | {{$name}}: 42 | installed: true 43 | versions: 44 | - {{$ver}} 45 | {{end}} 46 | addr: 47 | tcp://google.com:22: 48 | reachable: false 49 | timeout: 1000 50 | tcp://google.com:443: 51 | reachable: true 52 | timeout: 5000 53 | port: 54 | tcp:9999: 55 | listening: false 56 | user: 57 | root: 58 | exists: true 59 | foobar: 60 | exists: false 61 | group: 62 | foobar: 63 | exists: false 64 | dns: 65 | CAA:dnstest.io: 66 | resolveable: true 67 | addrs: 68 | - 0 issue comodoca.com 69 | - 0 issue letsencrypt.org 70 | - 0 issuewild ; 71 | timeout: 2000 72 | server: 8.8.8.8 73 | CNAME:c.dnstest.io: 74 | resolveable: true 75 | addrs: 76 | - a.dnstest.io. 77 | timeout: 2000 78 | server: 8.8.8.8 79 | c.dnstest.io: 80 | resolveable: true 81 | addrs: 82 | - 192.30.252.153 83 | timeout: 2000 84 | server: 8.8.8.8 85 | MX:dnstest.io: 86 | resolveable: true 87 | addrs: 88 | - 10 b.dnstest.io. 89 | - 5 a.dnstest.io. 90 | timeout: 2000 91 | server: 8.8.8.8 92 | NS:dnstest.io: 93 | resolveable: true 94 | addrs: 95 | - ns1.dnstest.io. 96 | - ns2.dnstest.io. 97 | timeout: 2000 98 | server: 8.8.8.8 99 | PTR:8.8.8.8: 100 | resolveable: true 101 | addrs: 102 | - google-public-dns-a.google.com. 103 | timeout: 2000 104 | server: 8.8.8.8 105 | SRV:_https._tcp.dnstest.io: 106 | resolveable: true 107 | addrs: 108 | - 0 5 443 a.dnstest.io. 109 | - 10 10 443 b.dnstest.io. 110 | timeout: 2000 111 | server: 8.8.8.8 112 | TXT:txt._test.dnstest.io: 113 | resolveable: true 114 | addrs: 115 | - Hello DNS 116 | timeout: 2000 117 | server: 8.8.8.8 118 | ip6.dnstest.io: 119 | resolveable: true 120 | addrs: 121 | - 2404:6800:4001:807::200e 122 | timeout: 2000 123 | server: 8.8.8.8 124 | localhost: 125 | resolveable: true 126 | addrs: 127 | - 127.0.0.1 128 | - "::1" 129 | timeout: 2000 130 | dnstest.io: 131 | resolveable: true 132 | server: 8.8.8.8 133 | timeout: 2000 134 | process: 135 | foobar: 136 | running: false 137 | kernel-param: 138 | kernel.ostype: 139 | value: Linux 140 | mount: 141 | "/dev": 142 | exists: true 143 | opts: 144 | - rw 145 | - nosuid 146 | source: tmpfs 147 | filesystem: tmpfs 148 | interface: 149 | eth0: 150 | exists: true 151 | addrs: 152 | contain-element: 153 | have-prefix: '172.17' 154 | http: 155 | https://www.google.com: 156 | status: 200 157 | allow-insecure: false 158 | timeout: 5000 159 | body: [] 160 | -------------------------------------------------------------------------------- /integration-tests/goss/centos7/goss-expected.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | mode: "0644" 5 | size: 745 6 | owner: root 7 | group: root 8 | filetype: file 9 | contains: [] 10 | /tmp/goss/foobar: 11 | exists: false 12 | contains: [] 13 | package: 14 | foobar: 15 | installed: false 16 | httpd: 17 | installed: true 18 | versions: 19 | - 2.4.6 20 | vim-tiny: 21 | installed: false 22 | addr: 23 | tcp://google.com:22: 24 | reachable: false 25 | timeout: 1000 26 | tcp://google.com:443: 27 | reachable: true 28 | timeout: 1000 29 | port: 30 | tcp:80: 31 | listening: false 32 | ip: [] 33 | tcp:9999: 34 | listening: false 35 | ip: [] 36 | tcp6:80: 37 | listening: true 38 | ip: 39 | - '::' 40 | service: 41 | foobar: 42 | enabled: false 43 | running: false 44 | httpd: 45 | enabled: true 46 | running: true 47 | user: 48 | apache: 49 | exists: true 50 | uid: 48 51 | gid: 48 52 | groups: 53 | - apache 54 | home: /usr/share/httpd 55 | shell: /sbin/nologin 56 | foobar: 57 | exists: false 58 | group: 59 | apache: 60 | exists: true 61 | gid: 48 62 | foobar: 63 | exists: false 64 | command: 65 | echo 'hi': 66 | exit-status: 0 67 | stdout: 68 | - hi 69 | stderr: [] 70 | timeout: 10000 71 | foobar: 72 | exit-status: 127 73 | stdout: [] 74 | stderr: 75 | - 'sh: foobar: command not found' 76 | timeout: 10000 77 | dns: 78 | CAA:dnstest.io: 79 | resolveable: true 80 | addrs: 81 | - 0 issue comodoca.com 82 | - 0 issue letsencrypt.org 83 | - 0 issuewild ; 84 | timeout: 1000 85 | server: 8.8.8.8 86 | CNAME:c.dnstest.io: 87 | resolveable: true 88 | addrs: 89 | - a.dnstest.io. 90 | timeout: 1000 91 | server: 8.8.8.8 92 | MX:dnstest.io: 93 | resolveable: true 94 | addrs: 95 | - 10 b.dnstest.io. 96 | - 5 a.dnstest.io. 97 | timeout: 1000 98 | server: 8.8.8.8 99 | NS:dnstest.io: 100 | resolveable: true 101 | addrs: 102 | - ns1.dnstest.io. 103 | - ns2.dnstest.io. 104 | timeout: 1000 105 | server: 8.8.8.8 106 | PTR:8.8.8.8: 107 | resolveable: true 108 | addrs: 109 | - google-public-dns-a.google.com. 110 | timeout: 1000 111 | server: 8.8.8.8 112 | SRV:_https._tcp.dnstest.io: 113 | resolveable: true 114 | addrs: 115 | - 0 5 443 a.dnstest.io. 116 | - 10 10 443 b.dnstest.io. 117 | timeout: 1000 118 | server: 8.8.8.8 119 | TXT:txt._test.dnstest.io: 120 | resolveable: true 121 | addrs: 122 | - Hello DNS 123 | timeout: 1000 124 | server: 8.8.8.8 125 | ip6.dnstest.io: 126 | resolveable: true 127 | addrs: 128 | - 2404:6800:4001:807::200e 129 | timeout: 1000 130 | server: 8.8.8.8 131 | localhost: 132 | resolveable: true 133 | addrs: 134 | - 127.0.0.1 135 | - ::1 136 | timeout: 1000 137 | process: 138 | foobar: 139 | running: false 140 | httpd: 141 | running: true 142 | kernel-param: 143 | kernel.ostype: 144 | value: Linux 145 | mount: 146 | /dev: 147 | exists: true 148 | opts: 149 | - rw 150 | - nosuid 151 | source: tmpfs 152 | filesystem: tmpfs 153 | http: 154 | http://google.com: 155 | status: 301 156 | allow-insecure: false 157 | no-follow-redirects: true 158 | timeout: 5000 159 | body: [] 160 | https://www.google.com: 161 | status: 200 162 | allow-insecure: false 163 | no-follow-redirects: false 164 | timeout: 5000 165 | body: [] 166 | -------------------------------------------------------------------------------- /integration-tests/goss/wheezy/goss-expected.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | mode: "0644" 5 | size: 761 6 | owner: root 7 | group: root 8 | filetype: file 9 | contains: [] 10 | /tmp/goss/foobar: 11 | exists: false 12 | contains: [] 13 | package: 14 | apache2: 15 | installed: true 16 | versions: 17 | - 2.2.22-13+deb7u7 18 | foobar: 19 | installed: false 20 | vim-tiny: 21 | installed: false 22 | addr: 23 | tcp://google.com:22: 24 | reachable: false 25 | timeout: 1000 26 | tcp://google.com:443: 27 | reachable: true 28 | timeout: 1000 29 | port: 30 | tcp:80: 31 | listening: false 32 | ip: [] 33 | tcp:9999: 34 | listening: false 35 | ip: [] 36 | tcp6:80: 37 | listening: true 38 | ip: 39 | - '::' 40 | service: 41 | apache2: 42 | enabled: true 43 | running: true 44 | foobar: 45 | enabled: false 46 | running: false 47 | user: 48 | foobar: 49 | exists: false 50 | www-data: 51 | exists: true 52 | uid: 33 53 | gid: 33 54 | groups: 55 | - www-data 56 | home: /var/www 57 | shell: /bin/sh 58 | group: 59 | foobar: 60 | exists: false 61 | www-data: 62 | exists: true 63 | gid: 33 64 | command: 65 | echo 'hi': 66 | exit-status: 0 67 | stdout: 68 | - hi 69 | stderr: [] 70 | timeout: 10000 71 | foobar: 72 | exit-status: 127 73 | stdout: [] 74 | stderr: 75 | - 'sh: 1: foobar: not found' 76 | timeout: 10000 77 | dns: 78 | CAA:dnstest.io: 79 | resolveable: true 80 | addrs: 81 | - 0 issue comodoca.com 82 | - 0 issue letsencrypt.org 83 | - 0 issuewild ; 84 | timeout: 1000 85 | server: 8.8.8.8 86 | CNAME:c.dnstest.io: 87 | resolveable: true 88 | addrs: 89 | - a.dnstest.io. 90 | timeout: 1000 91 | server: 8.8.8.8 92 | MX:dnstest.io: 93 | resolveable: true 94 | addrs: 95 | - 10 b.dnstest.io. 96 | - 5 a.dnstest.io. 97 | timeout: 1000 98 | server: 8.8.8.8 99 | NS:dnstest.io: 100 | resolveable: true 101 | addrs: 102 | - ns1.dnstest.io. 103 | - ns2.dnstest.io. 104 | timeout: 1000 105 | server: 8.8.8.8 106 | PTR:8.8.8.8: 107 | resolveable: true 108 | addrs: 109 | - google-public-dns-a.google.com. 110 | timeout: 1000 111 | server: 8.8.8.8 112 | SRV:_https._tcp.dnstest.io: 113 | resolveable: true 114 | addrs: 115 | - 0 5 443 a.dnstest.io. 116 | - 10 10 443 b.dnstest.io. 117 | timeout: 1000 118 | server: 8.8.8.8 119 | TXT:txt._test.dnstest.io: 120 | resolveable: true 121 | addrs: 122 | - Hello DNS 123 | timeout: 1000 124 | server: 8.8.8.8 125 | ip6.dnstest.io: 126 | resolveable: true 127 | addrs: 128 | - 2404:6800:4001:807::200e 129 | timeout: 1000 130 | server: 8.8.8.8 131 | localhost: 132 | resolveable: true 133 | addrs: 134 | - 127.0.0.1 135 | - ::1 136 | timeout: 1000 137 | process: 138 | apache2: 139 | running: true 140 | foobar: 141 | running: false 142 | kernel-param: 143 | kernel.ostype: 144 | value: Linux 145 | mount: 146 | /dev: 147 | exists: true 148 | opts: 149 | - rw 150 | - nosuid 151 | source: tmpfs 152 | filesystem: tmpfs 153 | http: 154 | http://google.com: 155 | status: 301 156 | allow-insecure: false 157 | no-follow-redirects: true 158 | timeout: 5000 159 | body: [] 160 | https://www.google.com: 161 | status: 200 162 | allow-insecure: false 163 | no-follow-redirects: false 164 | timeout: 5000 165 | body: [] 166 | -------------------------------------------------------------------------------- /integration-tests/goss/precise/goss-expected.yaml: -------------------------------------------------------------------------------- 1 | file: 2 | /etc/passwd: 3 | exists: true 4 | mode: "0644" 5 | size: 854 6 | owner: root 7 | group: root 8 | filetype: file 9 | contains: [] 10 | /tmp/goss/foobar: 11 | exists: false 12 | contains: [] 13 | package: 14 | apache2: 15 | installed: true 16 | versions: 17 | - 2.2.22-1ubuntu1.11 18 | foobar: 19 | installed: false 20 | vim-tiny: 21 | installed: false 22 | addr: 23 | tcp://google.com:22: 24 | reachable: false 25 | timeout: 1000 26 | tcp://google.com:443: 27 | reachable: true 28 | timeout: 1000 29 | port: 30 | tcp:80: 31 | listening: true 32 | ip: 33 | - 0.0.0.0 34 | tcp:9999: 35 | listening: false 36 | ip: [] 37 | tcp6:80: 38 | listening: false 39 | ip: [] 40 | service: 41 | apache2: 42 | enabled: false 43 | running: true 44 | foobar: 45 | enabled: false 46 | running: false 47 | user: 48 | foobar: 49 | exists: false 50 | www-data: 51 | exists: true 52 | uid: 33 53 | gid: 33 54 | groups: 55 | - www-data 56 | home: /var/www 57 | shell: /bin/sh 58 | group: 59 | foobar: 60 | exists: false 61 | www-data: 62 | exists: true 63 | gid: 33 64 | command: 65 | echo 'hi': 66 | exit-status: 0 67 | stdout: 68 | - hi 69 | stderr: [] 70 | timeout: 10000 71 | foobar: 72 | exit-status: 127 73 | stdout: [] 74 | stderr: 75 | - 'sh: 1: foobar: not found' 76 | timeout: 10000 77 | dns: 78 | CAA:dnstest.io: 79 | resolveable: true 80 | addrs: 81 | - 0 issue comodoca.com 82 | - 0 issue letsencrypt.org 83 | - 0 issuewild ; 84 | timeout: 1000 85 | server: 8.8.8.8 86 | CNAME:c.dnstest.io: 87 | resolveable: true 88 | addrs: 89 | - a.dnstest.io. 90 | timeout: 1000 91 | server: 8.8.8.8 92 | MX:dnstest.io: 93 | resolveable: true 94 | addrs: 95 | - 10 b.dnstest.io. 96 | - 5 a.dnstest.io. 97 | timeout: 1000 98 | server: 8.8.8.8 99 | NS:dnstest.io: 100 | resolveable: true 101 | addrs: 102 | - ns1.dnstest.io. 103 | - ns2.dnstest.io. 104 | timeout: 1000 105 | server: 8.8.8.8 106 | PTR:8.8.8.8: 107 | resolveable: true 108 | addrs: 109 | - google-public-dns-a.google.com. 110 | timeout: 1000 111 | server: 8.8.8.8 112 | SRV:_https._tcp.dnstest.io: 113 | resolveable: true 114 | addrs: 115 | - 0 5 443 a.dnstest.io. 116 | - 10 10 443 b.dnstest.io. 117 | timeout: 1000 118 | server: 8.8.8.8 119 | TXT:txt._test.dnstest.io: 120 | resolveable: true 121 | addrs: 122 | - Hello DNS 123 | timeout: 1000 124 | server: 8.8.8.8 125 | ip6.dnstest.io: 126 | resolveable: true 127 | addrs: 128 | - 2404:6800:4001:807::200e 129 | timeout: 1000 130 | server: 8.8.8.8 131 | localhost: 132 | resolveable: true 133 | addrs: 134 | - 127.0.0.1 135 | - ::1 136 | timeout: 1000 137 | process: 138 | apache2: 139 | running: true 140 | foobar: 141 | running: false 142 | kernel-param: 143 | kernel.ostype: 144 | value: Linux 145 | mount: 146 | /dev: 147 | exists: true 148 | opts: 149 | - rw 150 | - nosuid 151 | source: tmpfs 152 | filesystem: tmpfs 153 | http: 154 | http://google.com: 155 | status: 301 156 | allow-insecure: false 157 | no-follow-redirects: true 158 | timeout: 5000 159 | body: [] 160 | https://www.google.com: 161 | status: 200 162 | allow-insecure: false 163 | no-follow-redirects: false 164 | timeout: 5000 165 | body: [] 166 | -------------------------------------------------------------------------------- /resource/file.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/aelsabbahy/goss/system" 5 | "github.com/aelsabbahy/goss/util" 6 | ) 7 | 8 | type File struct { 9 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 10 | Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` 11 | Path string `json:"-" yaml:"-"` 12 | Exists matcher `json:"exists" yaml:"exists"` 13 | Mode matcher `json:"mode,omitempty" yaml:"mode,omitempty"` 14 | Size matcher `json:"size,omitempty" yaml:"size,omitempty"` 15 | Owner matcher `json:"owner,omitempty" yaml:"owner,omitempty"` 16 | Group matcher `json:"group,omitempty" yaml:"group,omitempty"` 17 | LinkedTo matcher `json:"linked-to,omitempty" yaml:"linked-to,omitempty"` 18 | Filetype matcher `json:"filetype,omitempty" yaml:"filetype,omitempty"` 19 | Contains []string `json:"contains" yaml:"contains"` 20 | Md5 matcher `json:"md5,omitempty" yaml:"md5,omitempty"` 21 | Sha256 matcher `json:"sha256,omitempty" yaml:"sha256,omitempty"` 22 | } 23 | 24 | func (f *File) ID() string { return f.Path } 25 | func (f *File) SetID(id string) { f.Path = id } 26 | 27 | func (f *File) GetTitle() string { return f.Title } 28 | func (f *File) GetMeta() meta { return f.Meta } 29 | 30 | func (f *File) Validate(sys *system.System) []TestResult { 31 | skip := false 32 | sysFile := sys.NewFile(f.Path, sys, util.Config{}) 33 | 34 | var results []TestResult 35 | results = append(results, ValidateValue(f, "exists", f.Exists, sysFile.Exists, skip)) 36 | if shouldSkip(results) { 37 | skip = true 38 | } 39 | if f.Mode != nil { 40 | results = append(results, ValidateValue(f, "mode", f.Mode, sysFile.Mode, skip)) 41 | } 42 | if f.Owner != nil { 43 | results = append(results, ValidateValue(f, "owner", f.Owner, sysFile.Owner, skip)) 44 | } 45 | if f.Group != nil { 46 | results = append(results, ValidateValue(f, "group", f.Group, sysFile.Group, skip)) 47 | } 48 | if f.LinkedTo != nil { 49 | results = append(results, ValidateValue(f, "linkedto", f.LinkedTo, sysFile.LinkedTo, skip)) 50 | } 51 | if f.Filetype != nil { 52 | results = append(results, ValidateValue(f, "filetype", f.Filetype, sysFile.Filetype, skip)) 53 | } 54 | if len(f.Contains) > 0 { 55 | results = append(results, ValidateContains(f, "contains", f.Contains, sysFile.Contains, skip)) 56 | } 57 | if f.Size != nil { 58 | results = append(results, ValidateValue(f, "size", f.Size, sysFile.Size, skip)) 59 | } 60 | if f.Md5 != nil { 61 | results = append(results, ValidateValue(f, "md5", f.Md5, sysFile.Md5, skip)) 62 | } 63 | if f.Sha256 != nil { 64 | results = append(results, ValidateValue(f, "sha256", f.Sha256, sysFile.Sha256, skip)) 65 | } 66 | return results 67 | } 68 | 69 | func NewFile(sysFile system.File, config util.Config) (*File, error) { 70 | path := sysFile.Path() 71 | exists, _ := sysFile.Exists() 72 | f := &File{ 73 | Path: path, 74 | Exists: exists, 75 | Contains: []string{}, 76 | } 77 | if !contains(config.IgnoreList, "mode") { 78 | if mode, err := sysFile.Mode(); err == nil { 79 | f.Mode = mode 80 | } 81 | } 82 | if !contains(config.IgnoreList, "owner") { 83 | if owner, err := sysFile.Owner(); err == nil { 84 | f.Owner = owner 85 | } 86 | } 87 | if !contains(config.IgnoreList, "group") { 88 | if group, err := sysFile.Group(); err == nil { 89 | f.Group = group 90 | } 91 | } 92 | if !contains(config.IgnoreList, "linked-to") { 93 | if linkedTo, err := sysFile.LinkedTo(); err == nil { 94 | f.LinkedTo = linkedTo 95 | } 96 | } 97 | if !contains(config.IgnoreList, "filetype") { 98 | if filetype, err := sysFile.Filetype(); err == nil { 99 | f.Filetype = filetype 100 | } 101 | } 102 | if !contains(config.IgnoreList, "size") { 103 | if size, err := sysFile.Size(); err == nil { 104 | f.Size = size 105 | } 106 | } 107 | return f, nil 108 | } 109 | -------------------------------------------------------------------------------- /extras/dgoss/README.md: -------------------------------------------------------------------------------- 1 | # dgoss 2 | 3 | dgoss is a convenience wrapper around goss that aims to bring the simplicity of goss to docker containers. 4 | 5 | ## Examples and Tutorials 6 | * [video tutorial](https://youtu.be/PEHz5EnZ-FM) - Introduction to dgoss tutorial 7 | * [blog tutorial](https://medium.com/@aelsabbahy/tutorial-how-to-test-your-docker-image-in-half-a-second-bbd13e06a4a9) - Same as above, but in written format 8 | * [dgoss-examples](https://github.com/aelsabbahy/dgoss-examples) - Repo containing examples of using dgoss to validate docker images 9 | 10 | ## Installation 11 | #### Linux: 12 | 13 | Follow the goss [installation instructions](https://github.com/aelsabbahy/goss#installation) 14 | 15 | #### Mac OSX 16 | 17 | Since goss runs on the target container, dgoss can be used on a Mac OSX system by doing the following: 18 | ``` 19 | # Install dgoss 20 | curl -L https://raw.githubusercontent.com/aelsabbahy/goss/master/extras/dgoss/dgoss -o /usr/local/bin/dgoss 21 | chmod +rx /usr/local/bin/dgoss 22 | 23 | # Download goss to your preferred location 24 | curl -L https://github.com/aelsabbahy/goss/releases/download/v0.3.3/goss-linux-amd64 -o ~/Downloads/goss-linux-amd64 25 | 26 | # Set your GOSS_PATH to the above location 27 | export GOSS_PATH=~/Downloads/goss-linux-amd64 28 | 29 | # Use dgoss 30 | dgoss edit ... 31 | dgoss run ... 32 | ``` 33 | 34 | 35 | ## Usage 36 | 37 | `dgoss [run|edit] ` 38 | 39 | 40 | ### Run 41 | 42 | Run is used to validate a docker container. It expects a `./goss.yaml` file to exist in the directory it was invoked from. In most cases one can just substitute the docker command for the dgoss command, for example: 43 | 44 | **run:** 45 | 46 | `docker run -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" jenkins:alpine` 47 | 48 | **test:** 49 | 50 | `dgoss run -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" jenkins:alpine` 51 | 52 | 53 | `dgoss run` will do the following: 54 | * Run the container with the flags you specified. 55 | * Stream the containers log output into the container as `/goss/docker_output.log` 56 | * This allows writing tests or waits against the docker output 57 | * (optional) Run `goss` with `$GOSS_WAIT_OPTS` if `./goss_wait.yaml` file exists in the current dir 58 | * Run `goss` with `$GOSS_OPTS` using `./goss.yaml` 59 | 60 | 61 | ### Edit 62 | 63 | Edit will launch a docker container, install goss, and drop the user into an interactive shell. Once the user quits the interactive shell, any `goss.yaml` or `goss_wait.yaml` are copied out into the current directory. This allows the user to leverage the `goss add|autoadd` commands to write tests as they would on a regular machine. 64 | 65 | **Example:** 66 | 67 | `dgoss edit -e JENKINS_OPTS="--httpPort=8080 --httpsPort=-1" -e JAVA_OPTS="-Xmx1048m" jenkins:alpine` 68 | 69 | ### Environment vars and defaults 70 | The following environment variables can be set to change the behavior of dgoss. 71 | 72 | ##### GOSS_PATH 73 | Location of the goss binary to use. (Default: `$(which goss)`) 74 | 75 | ##### GOSS_OPTS 76 | Options to use for the goss test run. (Default: `--color --format documentation`) 77 | 78 | ##### GOSS_WAIT_OPTS 79 | Options to use for the goss wait run, when `./goss_wait.yaml` exists. (Default: `-r 30s -s 1s > /dev/null`) 80 | 81 | ##### GOSS_SLEEP 82 | Time to sleep after running container (and optionally `goss_wait.yaml`) and before running tests. (Default: `0.2`) 83 | 84 | ##### GOSS_FILES_PATH 85 | Location of the goss yaml files. (Default: `.`) 86 | 87 | ##### GOSS_VARS 88 | The name of the variables file relative to `GOSS_FILES_PATH` to copy into the 89 | docker container and use for valiation (i.e. `dgoss run`) and copy out of the 90 | docker container when writing tests (i.e. `dgoss edit`). If set, the 91 | `--vars` flag is passed to `goss validate` commands inside the container. 92 | If unset (or empty), the `--vars` flag is omitted, which is the normal behavior. 93 | (Default: `''`). 94 | -------------------------------------------------------------------------------- /resource/validate_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type FakeResource struct { 11 | id string 12 | } 13 | 14 | func (f *FakeResource) ID() string { 15 | return f.id 16 | } 17 | func (f *FakeResource) GetTitle() string { return "title" } 18 | 19 | func (f *FakeResource) GetMeta() meta { return meta{"foo": "bar"} } 20 | 21 | var stringTests = []struct { 22 | in, in2 interface{} 23 | want bool 24 | }{ 25 | {"", "", true}, 26 | {"foo", "foo", true}, 27 | {"foo", "bar", false}, 28 | {"foo", "", false}, 29 | {true, true, true}, 30 | } 31 | 32 | func TestValidateValue(t *testing.T) { 33 | for _, c := range stringTests { 34 | inFunc := func() (interface{}, error) { 35 | return c.in2, nil 36 | } 37 | got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false) 38 | if got.Successful != c.want { 39 | t.Errorf("%+v: got %v, want %v", c, got.Successful, c.want) 40 | } 41 | } 42 | } 43 | 44 | func TestValidateValueErr(t *testing.T) { 45 | for _, c := range stringTests { 46 | inFunc := func() (interface{}, error) { 47 | return c.in2, fmt.Errorf("some err") 48 | } 49 | got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false) 50 | if got.Successful != false { 51 | t.Errorf("%+v: got %v, want %v", c, got.Successful, false) 52 | } 53 | } 54 | } 55 | 56 | func TestValidateValueSkip(t *testing.T) { 57 | for _, c := range stringTests { 58 | inFunc := func() (interface{}, error) { 59 | return c.in2, nil 60 | } 61 | got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, true) 62 | if got.Result != SKIP { 63 | t.Errorf("%+v: got %v, want %v", c, got.Result, SKIP) 64 | } 65 | } 66 | } 67 | 68 | func BenchmarkValidateValue(b *testing.B) { 69 | inFunc := func() (interface{}, error) { 70 | return "foo", nil 71 | } 72 | for n := 0; n < b.N; n++ { 73 | ValidateValue(&FakeResource{""}, "", "foo", inFunc, false) 74 | } 75 | } 76 | 77 | var containsTests = []struct { 78 | in []string 79 | in2 string 80 | want bool 81 | }{ 82 | {[]string{""}, "", true}, 83 | {[]string{"foo"}, "foo\nbar", true}, 84 | {[]string{"!foo"}, "foo\nbar", false}, 85 | {[]string{"!moo"}, "foo\nbar", true}, 86 | {[]string{"/fo.*/"}, "foo\nbar", true}, 87 | {[]string{"!/fo.*/"}, "foo\nbar", false}, 88 | {[]string{"!/mo.*/"}, "foo\nbar", true}, 89 | {[]string{"foo"}, "", false}, 90 | {[]string{`/\s/tmp\b/`}, "test /tmp bar", true}, 91 | } 92 | 93 | func TestValidateContains(t *testing.T) { 94 | for _, c := range containsTests { 95 | inFunc := func() (io.Reader, error) { 96 | reader := strings.NewReader(c.in2) 97 | return reader, nil 98 | } 99 | got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, false) 100 | if got.Successful != c.want { 101 | t.Errorf("%+v: got %v, want %v", c, got.Successful, c.want) 102 | } 103 | } 104 | } 105 | 106 | func TestValidateContainsErr(t *testing.T) { 107 | for _, c := range containsTests { 108 | inFunc := func() (io.Reader, error) { 109 | reader := strings.NewReader(c.in2) 110 | return reader, fmt.Errorf("some err") 111 | } 112 | got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, false) 113 | if got.Successful != false { 114 | t.Errorf("%+v: got %v, want %v", c, got.Successful, false) 115 | } 116 | } 117 | } 118 | 119 | func TestValidateContainsBadRegexErr(t *testing.T) { 120 | inFunc := func() (io.Reader, error) { 121 | reader := strings.NewReader("dummy") 122 | return reader, nil 123 | } 124 | got := ValidateContains(&FakeResource{""}, "", []string{"/*\\.* @@.*/"}, inFunc, false) 125 | if got.Err == nil { 126 | t.Errorf("Expected bad regex to raise error, got nil") 127 | } 128 | } 129 | 130 | func TestValidateContainsSkip(t *testing.T) { 131 | for _, c := range containsTests { 132 | inFunc := func() (io.Reader, error) { 133 | reader := strings.NewReader(c.in2) 134 | return reader, nil 135 | } 136 | got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, true) 137 | if got.Result != SKIP { 138 | t.Errorf("%+v: got %v, want %v", c, got.Result, SKIP) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /goss_config.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/aelsabbahy/goss/resource" 7 | ) 8 | 9 | type GossConfig struct { 10 | Files resource.FileMap `json:"file,omitempty" yaml:"file,omitempty"` 11 | Packages resource.PackageMap `json:"package,omitempty" yaml:"package,omitempty"` 12 | Addrs resource.AddrMap `json:"addr,omitempty" yaml:"addr,omitempty"` 13 | Ports resource.PortMap `json:"port,omitempty" yaml:"port,omitempty"` 14 | Services resource.ServiceMap `json:"service,omitempty" yaml:"service,omitempty"` 15 | Users resource.UserMap `json:"user,omitempty" yaml:"user,omitempty"` 16 | Groups resource.GroupMap `json:"group,omitempty" yaml:"group,omitempty"` 17 | Commands resource.CommandMap `json:"command,omitempty" yaml:"command,omitempty"` 18 | DNS resource.DNSMap `json:"dns,omitempty" yaml:"dns,omitempty"` 19 | Processes resource.ProcessMap `json:"process,omitempty" yaml:"process,omitempty"` 20 | Gossfiles resource.GossfileMap `json:"gossfile,omitempty" yaml:"gossfile,omitempty"` 21 | KernelParams resource.KernelParamMap `json:"kernel-param,omitempty" yaml:"kernel-param,omitempty"` 22 | Mounts resource.MountMap `json:"mount,omitempty" yaml:"mount,omitempty"` 23 | Interfaces resource.InterfaceMap `json:"interface,omitempty" yaml:"interface,omitempty"` 24 | HTTPs resource.HTTPMap `json:"http,omitempty" yaml:"http,omitempty"` 25 | Matchings resource.MatchingMap `json:"matching,omitempty" yaml:"matching,omitempty"` 26 | } 27 | 28 | func NewGossConfig() *GossConfig { 29 | return &GossConfig{ 30 | Files: make(resource.FileMap), 31 | Packages: make(resource.PackageMap), 32 | Addrs: make(resource.AddrMap), 33 | Ports: make(resource.PortMap), 34 | Services: make(resource.ServiceMap), 35 | Users: make(resource.UserMap), 36 | Groups: make(resource.GroupMap), 37 | Commands: make(resource.CommandMap), 38 | DNS: make(resource.DNSMap), 39 | Processes: make(resource.ProcessMap), 40 | Gossfiles: make(resource.GossfileMap), 41 | KernelParams: make(resource.KernelParamMap), 42 | Mounts: make(resource.MountMap), 43 | Interfaces: make(resource.InterfaceMap), 44 | HTTPs: make(resource.HTTPMap), 45 | } 46 | } 47 | 48 | func (c *GossConfig) Resources() []resource.Resource { 49 | var tests []resource.Resource 50 | 51 | gm := genericConcatMaps(c.Commands, 52 | c.HTTPs, 53 | c.Addrs, 54 | c.DNS, 55 | c.Packages, 56 | c.Services, 57 | c.Files, 58 | c.Processes, 59 | c.Users, 60 | c.Groups, 61 | c.Ports, 62 | c.KernelParams, 63 | c.Mounts, 64 | c.Interfaces, 65 | c.Matchings, 66 | ) 67 | 68 | for _, m := range gm { 69 | for _, t := range m { 70 | // FIXME: Can this be moved to a safer compile-time check? 71 | tests = append(tests, t.(resource.Resource)) 72 | } 73 | } 74 | 75 | return tests 76 | } 77 | 78 | func genericConcatMaps(maps ...interface{}) (ret []map[string]interface{}) { 79 | for _, slice := range maps { 80 | im := interfaceMap(slice) 81 | ret = append(ret, im) 82 | } 83 | return ret 84 | } 85 | 86 | func interfaceMap(slice interface{}) map[string]interface{} { 87 | m := reflect.ValueOf(slice) 88 | if m.Kind() != reflect.Map { 89 | panic("InterfaceSlice() given a non-slice type") 90 | } 91 | 92 | ret := make(map[string]interface{}) 93 | 94 | for _, k := range m.MapKeys() { 95 | ret[k.Interface().(string)] = m.MapIndex(k).Interface() 96 | } 97 | 98 | return ret 99 | } 100 | 101 | func mergeGoss(g1, g2 GossConfig) GossConfig { 102 | g1.Gossfiles = nil 103 | 104 | for k, v := range g2.Files { 105 | g1.Files[k] = v 106 | } 107 | 108 | for k, v := range g2.Packages { 109 | g1.Packages[k] = v 110 | } 111 | 112 | for k, v := range g2.Addrs { 113 | g1.Addrs[k] = v 114 | } 115 | 116 | for k, v := range g2.Ports { 117 | g1.Ports[k] = v 118 | } 119 | 120 | for k, v := range g2.Services { 121 | g1.Services[k] = v 122 | } 123 | 124 | for k, v := range g2.Users { 125 | g1.Users[k] = v 126 | } 127 | 128 | for k, v := range g2.Groups { 129 | g1.Groups[k] = v 130 | } 131 | 132 | for k, v := range g2.Commands { 133 | g1.Commands[k] = v 134 | } 135 | 136 | for k, v := range g2.DNS { 137 | g1.DNS[k] = v 138 | } 139 | 140 | for k, v := range g2.Processes { 141 | g1.Processes[k] = v 142 | } 143 | 144 | for k, v := range g2.KernelParams { 145 | g1.KernelParams[k] = v 146 | } 147 | 148 | for k, v := range g2.Mounts { 149 | g1.Mounts[k] = v 150 | } 151 | 152 | for k, v := range g2.Interfaces { 153 | g1.Interfaces[k] = v 154 | } 155 | 156 | for k, v := range g2.HTTPs { 157 | g1.HTTPs[k] = v 158 | } 159 | 160 | return g1 161 | } 162 | -------------------------------------------------------------------------------- /resource/gomega_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/types" 12 | ) 13 | 14 | var gomegaTests = []struct { 15 | in string 16 | want interface{} 17 | useNegateTester bool 18 | }{ 19 | // Default for simple types 20 | { 21 | in: `"foo"`, 22 | want: gomega.Equal("foo"), 23 | }, 24 | { 25 | in: `1`, 26 | want: gomega.Equal(float64(1)), 27 | }, 28 | { 29 | in: `true`, 30 | want: gomega.Equal(true), 31 | }, 32 | // Default for Array 33 | { 34 | in: `["foo", "bar"]`, 35 | want: gomega.And(gomega.ContainElement("foo"), gomega.ContainElement("bar")), 36 | useNegateTester: true, 37 | }, 38 | 39 | // Numeric 40 | // Golang json escapes '>', '<' symbols, so we use 'gt', 'le' instead 41 | { 42 | in: `{"gt": 1}`, 43 | want: gomega.BeNumerically(">", float64(1)), 44 | }, 45 | { 46 | in: `{"ge": 1}`, 47 | want: gomega.BeNumerically(">=", float64(1)), 48 | }, 49 | { 50 | in: `{"lt": 1}`, 51 | want: gomega.BeNumerically("<", float64(1)), 52 | }, 53 | { 54 | in: `{"le": 1}`, 55 | want: gomega.BeNumerically("<=", float64(1)), 56 | }, 57 | 58 | // String 59 | { 60 | in: `{"have-prefix": "foo"}`, 61 | want: gomega.HavePrefix("foo"), 62 | }, 63 | { 64 | in: `{"have-suffix": "foo"}`, 65 | want: gomega.HaveSuffix("foo"), 66 | }, 67 | // Regex support is based on golangs regex engine https://golang.org/pkg/regexp/syntax/ 68 | { 69 | in: `{"match-regexp": "foo"}`, 70 | want: gomega.MatchRegexp("foo"), 71 | }, 72 | 73 | // Collection 74 | { 75 | in: `{"consist-of": ["foo"]}`, 76 | want: gomega.ConsistOf(gomega.Equal("foo")), 77 | }, 78 | { 79 | in: `{"contain-element": "foo"}`, 80 | want: gomega.ContainElement(gomega.Equal("foo")), 81 | }, 82 | { 83 | in: `{"have-len": 3}`, 84 | want: gomega.HaveLen(3), 85 | }, 86 | { 87 | in: `{"have-key-with-value": { "foo": 1, "bar": "baz" }}`, 88 | // Keys are sorted and then passed to gomega.And so the order 89 | // of the conditions in this `want` is important 90 | want: gomega.And( 91 | gomega.HaveKeyWithValue("bar", gomega.Equal("baz")), 92 | gomega.HaveKeyWithValue("foo", gomega.Equal(1)), 93 | ), 94 | useNegateTester: true, 95 | }, 96 | { 97 | in: `{"have-key": "foo"}`, 98 | want: gomega.HaveKey(gomega.Equal("foo")), 99 | }, 100 | 101 | // Negation 102 | { 103 | in: `{"not": "foo"}`, 104 | want: gomega.Not(gomega.Equal("foo")), 105 | }, 106 | // Complex logic 107 | { 108 | in: `{"and": ["foo", "foo"]}`, 109 | want: gomega.And(gomega.Equal("foo"), gomega.Equal("foo")), 110 | useNegateTester: true, 111 | }, 112 | { 113 | in: `{"and": [{"have-prefix": "foo"}, "foo"]}`, 114 | want: gomega.And(gomega.HavePrefix("foo"), gomega.Equal("foo")), 115 | useNegateTester: true, 116 | }, 117 | { 118 | in: `{"not": {"have-prefix": "foo"}}`, 119 | want: gomega.Not(gomega.HavePrefix("foo")), 120 | }, 121 | { 122 | in: `{"or": ["foo", "foo"]}`, 123 | want: gomega.Or(gomega.Equal("foo"), gomega.Equal("foo")), 124 | }, 125 | { 126 | in: `{"not": {"and": [{"have-prefix": "foo"}]}}`, 127 | want: gomega.Not(gomega.And(gomega.HavePrefix("foo"))), 128 | }, 129 | } 130 | 131 | func TestMatcherToGomegaMatcher(t *testing.T) { 132 | for _, c := range gomegaTests { 133 | var dat interface{} 134 | if err := json.Unmarshal([]byte(c.in), &dat); err != nil { 135 | t.Fatal(err) 136 | } 137 | got, err := matcherToGomegaMatcher(dat) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | gomegaTestEqual(t, got, c.want, c.useNegateTester, c.in) 142 | } 143 | } 144 | 145 | func gomegaTestEqual(t *testing.T, got, want interface{}, useNegateTester bool, in string) { 146 | if !gomegaEqual(got, want, useNegateTester) { 147 | t.Errorf("For input '%s': got %T %v, want %T %v", in, got, got, want, want) 148 | } 149 | } 150 | func gomegaEqual(g, w interface{}, negateTester bool) bool { 151 | gotT := reflect.TypeOf(g) 152 | wantT := reflect.TypeOf(w) 153 | got := g.(types.GomegaMatcher) 154 | want := w.(types.GomegaMatcher) 155 | var gotMessage string 156 | var wantMessage string 157 | if negateTester { 158 | gotMessage = got.NegatedFailureMessage("foo") 159 | wantMessage = want.NegatedFailureMessage("foo") 160 | } else { 161 | gotMessage = got.FailureMessage("foo") 162 | wantMessage = want.FailureMessage("foo") 163 | } 164 | gotMessage = sanitizeMatcherText(gotMessage) 165 | wantMessage = sanitizeMatcherText(wantMessage) 166 | fmt.Println("got:", gotMessage) 167 | fmt.Println("want:", wantMessage) 168 | 169 | return gotT == wantT && 170 | gotMessage == wantMessage 171 | } 172 | 173 | func sanitizeMatcherText(s string) string { 174 | r := regexp.MustCompile("[0-9]x[a-z0-9]{10}") 175 | return r.ReplaceAllString(s, "") 176 | } 177 | -------------------------------------------------------------------------------- /resource/gomega.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/onsi/gomega" 8 | "github.com/onsi/gomega/types" 9 | ) 10 | 11 | func matcherToGomegaMatcher(matcher interface{}) (types.GomegaMatcher, error) { 12 | switch x := matcher.(type) { 13 | case string, int, bool, float64: 14 | return gomega.Equal(x), nil 15 | case []interface{}: 16 | var matchers []types.GomegaMatcher 17 | for _, valueI := range x { 18 | if subMatcher, ok := valueI.(types.GomegaMatcher); ok { 19 | matchers = append(matchers, subMatcher) 20 | } else { 21 | matchers = append(matchers, gomega.ContainElement(valueI)) 22 | } 23 | } 24 | return gomega.And(matchers...), nil 25 | } 26 | matcher = sanitizeExpectedValue(matcher) 27 | if matcher == nil { 28 | return nil, fmt.Errorf("Missing Required Attribute") 29 | } 30 | matcherMap, ok := matcher.(map[string]interface{}) 31 | if !ok { 32 | panic(fmt.Sprintf("Unexpected matcher type: %T\n\n", matcher)) 33 | } 34 | var matchType string 35 | var value interface{} 36 | for matchType, value = range matcherMap { 37 | break 38 | } 39 | switch matchType { 40 | case "have-prefix": 41 | return gomega.HavePrefix(value.(string)), nil 42 | case "have-suffix": 43 | return gomega.HaveSuffix(value.(string)), nil 44 | case "match-regexp": 45 | return gomega.MatchRegexp(value.(string)), nil 46 | case "have-len": 47 | value = sanitizeExpectedValue(value) 48 | return gomega.HaveLen(value.(int)), nil 49 | case "have-key-with-value": 50 | subMatchers, err := mapToGomega(value) 51 | if err != nil { 52 | return nil, err 53 | } 54 | for key, val := range subMatchers { 55 | if val == nil { 56 | fmt.Printf("%d is nil", key) 57 | } 58 | } 59 | return gomega.And(subMatchers...), nil 60 | case "have-key": 61 | subMatcher, err := matcherToGomegaMatcher(value) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return gomega.HaveKey(subMatcher), nil 66 | case "contain-element": 67 | subMatcher, err := matcherToGomegaMatcher(value) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return gomega.ContainElement(subMatcher), nil 72 | case "not": 73 | subMatcher, err := matcherToGomegaMatcher(value) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return gomega.Not(subMatcher), nil 78 | case "consist-of": 79 | subMatchers, err := sliceToGomega(value) 80 | if err != nil { 81 | return nil, err 82 | } 83 | var interfaceSlice []interface{} 84 | for _, d := range subMatchers { 85 | interfaceSlice = append(interfaceSlice, d) 86 | } 87 | return gomega.ConsistOf(interfaceSlice...), nil 88 | case "and": 89 | subMatchers, err := sliceToGomega(value) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return gomega.And(subMatchers...), nil 94 | case "or": 95 | subMatchers, err := sliceToGomega(value) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return gomega.Or(subMatchers...), nil 100 | case "gt", "ge", "lt", "le": 101 | // Golang json escapes '>', '<' symbols, so we use 'gt', 'le' instead 102 | comparator := map[string]string{ 103 | "gt": ">", 104 | "ge": ">=", 105 | "lt": "<", 106 | "le": "<=", 107 | }[matchType] 108 | return gomega.BeNumerically(comparator, value), nil 109 | 110 | default: 111 | return nil, fmt.Errorf("Unknown matcher: %s", matchType) 112 | 113 | } 114 | } 115 | 116 | func mapToGomega(value interface{}) (subMatchers []types.GomegaMatcher, err error) { 117 | valueI, ok := value.(map[string]interface{}) 118 | if !ok { 119 | return nil, fmt.Errorf("Matcher expected map, got: %t", value) 120 | } 121 | 122 | // Get keys 123 | keys := []string{} 124 | for key, _ := range valueI { 125 | keys = append(keys, key) 126 | } 127 | // Iterate through keys in a deterministic way, since ranging over a map 128 | // does not guarantee order 129 | sort.Strings(keys) 130 | for _, key := range keys { 131 | val := valueI[key] 132 | val, err = matcherToGomegaMatcher(val) 133 | if err != nil { 134 | return 135 | } 136 | 137 | subMatcher := gomega.HaveKeyWithValue(key, val) 138 | subMatchers = append(subMatchers, subMatcher) 139 | } 140 | return 141 | } 142 | 143 | func sliceToGomega(value interface{}) ([]types.GomegaMatcher, error) { 144 | valueI, ok := value.([]interface{}) 145 | if !ok { 146 | return nil, fmt.Errorf("Matcher expected array, got: %t", value) 147 | } 148 | var subMatchers []types.GomegaMatcher 149 | for _, v := range valueI { 150 | subMatcher, err := matcherToGomegaMatcher(v) 151 | if err != nil { 152 | return nil, err 153 | } 154 | subMatchers = append(subMatchers, subMatcher) 155 | } 156 | return subMatchers, nil 157 | } 158 | 159 | // Normalize expectedValue so json and yaml are the same 160 | func sanitizeExpectedValue(i interface{}) interface{} { 161 | if e, ok := i.(float64); ok { 162 | return int(e) 163 | } 164 | if e, ok := i.(map[interface{}]interface{}); ok { 165 | out := make(map[string]interface{}) 166 | for k, v := range e { 167 | ks, ok := k.(string) 168 | if !ok { 169 | panic(fmt.Sprintf("Matcher key type not string: %T\n\n", k)) 170 | } 171 | out[ks] = sanitizeExpectedValue(v) 172 | } 173 | return out 174 | } 175 | return i 176 | } 177 | -------------------------------------------------------------------------------- /outputs/outputs.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sort" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/aelsabbahy/goss/resource" 13 | "github.com/fatih/color" 14 | ) 15 | 16 | type Outputer interface { 17 | Output(io.Writer, <-chan []resource.TestResult, time.Time) int 18 | } 19 | 20 | var green = color.New(color.FgGreen).SprintfFunc() 21 | var red = color.New(color.FgRed).SprintfFunc() 22 | var yellow = color.New(color.FgYellow).SprintfFunc() 23 | 24 | func humanizeResult(r resource.TestResult) string { 25 | if r.Err != nil { 26 | return red("%s: %s: Error: %s", r.ResourceId, r.Property, r.Err) 27 | } 28 | 29 | switch r.Result { 30 | case resource.SUCCESS: 31 | return green("%s: %s: %s: matches expectation: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected) 32 | case resource.SKIP: 33 | return yellow("%s: %s: %s: skipped", r.ResourceType, r.ResourceId, r.Property) 34 | case resource.FAIL: 35 | if r.Human != "" { 36 | return red("%s: %s: %s:\n%s", r.ResourceType, r.ResourceId, r.Property, r.Human) 37 | } 38 | return humanizeResult2(r) 39 | default: 40 | panic(fmt.Sprintf("Unexpected Result Code: %v\n", r.Result)) 41 | } 42 | } 43 | 44 | func humanizeResult2(r resource.TestResult) string { 45 | if r.Err != nil { 46 | return red("%s: %s: Error: %s", r.ResourceId, r.Property, r.Err) 47 | } 48 | 49 | switch r.Result { 50 | case resource.SUCCESS: 51 | switch r.TestType { 52 | case resource.Value: 53 | return green("%s: %s: %s: matches expectation: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected) 54 | case resource.Values: 55 | return green("%s: %s: %s: all expectations found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(r.Expected, ", ")) 56 | case resource.Contains: 57 | return green("%s: %s: %s: all expectations found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(r.Expected, ", ")) 58 | default: 59 | return red("Unexpected type %d", r.TestType) 60 | } 61 | case resource.FAIL: 62 | switch r.TestType { 63 | case resource.Value: 64 | return red("%s: %s: %s: doesn't match, expect: %s found: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected, r.Found) 65 | case resource.Values: 66 | return red("%s: %s: %s: expectations not found [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(subtractSlice(r.Expected, r.Found), ", ")) 67 | case resource.Contains: 68 | return red("%s: %s: %s: patterns not found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(subtractSlice(r.Expected, r.Found), ", ")) 69 | default: 70 | return red("Unexpected type %d", r.TestType) 71 | } 72 | case resource.SKIP: 73 | return yellow("%s: %s: %s: skipped", r.ResourceType, r.ResourceId, r.Property) 74 | default: 75 | panic(fmt.Sprintf("Unexpected Result Code: %v\n", r.Result)) 76 | } 77 | } 78 | 79 | // Copied from database/sql 80 | var ( 81 | outputersMu sync.Mutex 82 | outputers = make(map[string]Outputer) 83 | ) 84 | 85 | func RegisterOutputer(name string, outputer Outputer) { 86 | outputersMu.Lock() 87 | defer outputersMu.Unlock() 88 | 89 | if outputer == nil { 90 | panic("goss: Register outputer is nil") 91 | } 92 | if _, dup := outputers[name]; dup { 93 | panic("goss: Register called twice for ouputer " + name) 94 | } 95 | outputers[name] = outputer 96 | } 97 | 98 | // Outputers returns a sorted list of the names of the registered outputers. 99 | func Outputers() []string { 100 | outputersMu.Lock() 101 | defer outputersMu.Unlock() 102 | var list []string 103 | for name := range outputers { 104 | list = append(list, name) 105 | } 106 | sort.Strings(list) 107 | return list 108 | } 109 | 110 | func GetOutputer(name string) Outputer { 111 | if _, ok := outputers[name]; !ok { 112 | fmt.Println("goss: Bad output format: " + name) 113 | os.Exit(1) 114 | } 115 | return outputers[name] 116 | } 117 | 118 | func subtractSlice(x, y []string) []string { 119 | m := make(map[string]bool) 120 | 121 | for _, y := range y { 122 | m[y] = true 123 | } 124 | 125 | var ret []string 126 | for _, x := range x { 127 | if m[x] { 128 | continue 129 | } 130 | ret = append(ret, x) 131 | } 132 | 133 | return ret 134 | } 135 | 136 | func header(t resource.TestResult) string { 137 | var out string 138 | if t.Title != "" { 139 | out += fmt.Sprintf("Title: %s\n", t.Title) 140 | } 141 | if t.Meta != nil { 142 | var keys []string 143 | for k := range t.Meta { 144 | keys = append(keys, k) 145 | } 146 | sort.Strings(keys) 147 | 148 | out += "Meta:\n" 149 | for _, k := range keys { 150 | out += fmt.Sprintf(" %v: %v\n", k, t.Meta[k]) 151 | } 152 | } 153 | return out 154 | } 155 | 156 | func summary(startTime time.Time, count, failed, skipped int) string { 157 | var s string 158 | s += fmt.Sprintf("Total Duration: %.3fs\n", time.Since(startTime).Seconds()) 159 | f := green 160 | if failed > 0 { 161 | f = red 162 | } 163 | s += f("Count: %d, Failed: %d, Skipped: %d\n", count, failed, skipped) 164 | return s 165 | } 166 | func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult) string { 167 | var s string 168 | if len(failedOrSkipped) > 0 { 169 | s += fmt.Sprint("Failures/Skipped:\n\n") 170 | for _, failedGroup := range failedOrSkipped { 171 | first := failedGroup[0] 172 | header := header(first) 173 | if header != "" { 174 | s += fmt.Sprint(header) 175 | } 176 | for _, testResult := range failedGroup { 177 | s += fmt.Sprintln(humanizeResult(testResult)) 178 | } 179 | s += fmt.Sprint("\n") 180 | } 181 | } 182 | return s 183 | } 184 | -------------------------------------------------------------------------------- /system/system.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "sync" 9 | 10 | "github.com/aelsabbahy/GOnetstat" 11 | // This needs a better name 12 | util2 "github.com/aelsabbahy/goss/util" 13 | "github.com/mitchellh/go-ps" 14 | "github.com/urfave/cli" 15 | ) 16 | 17 | type Resource interface { 18 | Exists() (bool, error) 19 | } 20 | 21 | type System struct { 22 | NewPackage func(string, *System, util2.Config) Package 23 | NewFile func(string, *System, util2.Config) File 24 | NewAddr func(string, *System, util2.Config) Addr 25 | NewPort func(string, *System, util2.Config) Port 26 | NewService func(string, *System, util2.Config) Service 27 | NewUser func(string, *System, util2.Config) User 28 | NewGroup func(string, *System, util2.Config) Group 29 | NewCommand func(string, *System, util2.Config) Command 30 | NewDNS func(string, *System, util2.Config) DNS 31 | NewProcess func(string, *System, util2.Config) Process 32 | NewGossfile func(string, *System, util2.Config) Gossfile 33 | NewKernelParam func(string, *System, util2.Config) KernelParam 34 | NewMount func(string, *System, util2.Config) Mount 35 | NewInterface func(string, *System, util2.Config) Interface 36 | NewHTTP func(string, *System, util2.Config) HTTP 37 | ports map[string][]GOnetstat.Process 38 | portsOnce sync.Once 39 | procMap map[string][]ps.Process 40 | procOnce sync.Once 41 | } 42 | 43 | func (s *System) Ports() map[string][]GOnetstat.Process { 44 | s.portsOnce.Do(func() { 45 | s.ports = GetPorts(false) 46 | }) 47 | return s.ports 48 | } 49 | 50 | func (s *System) ProcMap() map[string][]ps.Process { 51 | s.procOnce.Do(func() { 52 | s.procMap = GetProcs() 53 | }) 54 | return s.procMap 55 | } 56 | 57 | func New(c *cli.Context) *System { 58 | sys := &System{ 59 | NewFile: NewDefFile, 60 | NewAddr: NewDefAddr, 61 | NewPort: NewDefPort, 62 | NewUser: NewDefUser, 63 | NewGroup: NewDefGroup, 64 | NewCommand: NewDefCommand, 65 | NewDNS: NewDefDNS, 66 | NewProcess: NewDefProcess, 67 | NewGossfile: NewDefGossfile, 68 | NewKernelParam: NewDefKernelParam, 69 | NewMount: NewDefMount, 70 | NewInterface: NewDefInterface, 71 | NewHTTP: NewDefHTTP, 72 | } 73 | sys.detectService() 74 | sys.detectPackage(c) 75 | return sys 76 | } 77 | 78 | // detectPackage adds the correct package creation function to a System struct 79 | func (sys *System) detectPackage(c *cli.Context) { 80 | p := c.GlobalString("package") 81 | if p != "deb" && p != "apk" && p != "pacman" && p != "rpm" { 82 | p = DetectPackageManager() 83 | } 84 | switch p { 85 | case "deb": 86 | sys.NewPackage = NewDebPackage 87 | case "apk": 88 | sys.NewPackage = NewAlpinePackage 89 | case "pacman": 90 | sys.NewPackage = NewPacmanPackage 91 | default: 92 | sys.NewPackage = NewRpmPackage 93 | } 94 | } 95 | 96 | // detectService adds the correct service creation function to a System struct 97 | func (sys *System) detectService() { 98 | switch DetectService() { 99 | case "upstart": 100 | sys.NewService = NewServiceUpstart 101 | case "systemd": 102 | sys.NewService = NewServiceSystemd 103 | case "alpineinit": 104 | sys.NewService = NewAlpineServiceInit 105 | default: 106 | sys.NewService = NewServiceInit 107 | } 108 | } 109 | 110 | // DetectPackageManager attempts to detect whether or not the system is using 111 | // "deb", "rpm", "apk", or "pacman" package managers. It first attempts to 112 | // detect the distro. If that fails, it falls back to finding package manager 113 | // executables. If that fails, it returns the empty string. 114 | func DetectPackageManager() string { 115 | switch DetectDistro() { 116 | case "ubuntu": 117 | return "deb" 118 | case "redhat": 119 | return "rpm" 120 | case "alpine": 121 | return "apk" 122 | case "arch": 123 | return "pacman" 124 | case "debian": 125 | return "deb" 126 | } 127 | for _, manager := range []string{"deb", "rpm", "apk", "pacman"} { 128 | if HasCommand(manager) { 129 | return manager 130 | } 131 | } 132 | return "" 133 | } 134 | 135 | // DetectService attempts to detect what kind of service management the system 136 | // is using, "systemd", "upstart", "alpineinit", or "init". It looks for systemctl 137 | // command to detect systemd, and falls back on DetectDistro otherwise. If it can't 138 | // decide, it returns "init". 139 | func DetectService() string { 140 | if HasCommand("systemctl") { 141 | return "systemd" 142 | } 143 | // Centos Docker container doesn't run systemd, so we detect it or use init. 144 | switch DetectDistro() { 145 | case "ubuntu": 146 | return "upstart" 147 | case "alpine": 148 | return "alpineinit" 149 | case "arch": 150 | return "systemd" 151 | } 152 | return "init" 153 | } 154 | 155 | // DetectDistro attempts to detect which Linux distribution this computer is 156 | // using. One of "ubuntu", "redhat" (including Centos), "alpine", "arch", or 157 | // "debian". If it can't decide, it returns an empty string. 158 | func DetectDistro() string { 159 | if b, e := ioutil.ReadFile("/etc/lsb-release"); e == nil && bytes.Contains(b, []byte("Ubuntu")) { 160 | return "ubuntu" 161 | } else if isRedhat() { 162 | return "redhat" 163 | } else if _, err := os.Stat("/etc/alpine-release"); err == nil { 164 | return "alpine" 165 | } else if _, err := os.Stat("/etc/arch-release"); err == nil { 166 | return "arch" 167 | } else if _, err := os.Stat("/etc/debian_version"); err == nil { 168 | return "debian" 169 | } 170 | return "" 171 | } 172 | 173 | // HasCommand returns whether or not an executable by this name is on the PATH. 174 | func HasCommand(cmd string) bool { 175 | if _, err := exec.LookPath(cmd); err == nil { 176 | return true 177 | } 178 | return false 179 | } 180 | 181 | func isRedhat() bool { 182 | if _, err := os.Stat("/etc/redhat-release"); err == nil { 183 | return true 184 | } else if _, err := os.Stat("/etc/system-release"); err == nil { 185 | return true 186 | } 187 | return false 188 | } 189 | -------------------------------------------------------------------------------- /system/file.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha256" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/aelsabbahy/goss/util" 15 | "github.com/opencontainers/runc/libcontainer/user" 16 | ) 17 | 18 | type File interface { 19 | Path() string 20 | Exists() (bool, error) 21 | Contains() (io.Reader, error) 22 | Mode() (string, error) 23 | Size() (int, error) 24 | Filetype() (string, error) 25 | Owner() (string, error) 26 | Group() (string, error) 27 | LinkedTo() (string, error) 28 | Md5() (string, error) 29 | Sha256() (string, error) 30 | } 31 | 32 | type DefFile struct { 33 | path string 34 | realPath string 35 | fi os.FileInfo 36 | loaded bool 37 | err error 38 | } 39 | 40 | func NewDefFile(path string, system *System, config util.Config) File { 41 | if !strings.HasPrefix(path, "~") { 42 | // FIXME: we probably shouldn't ignore errors here 43 | path, _ = filepath.Abs(path) 44 | } 45 | return &DefFile{path: path} 46 | } 47 | 48 | func (f *DefFile) setup() error { 49 | if f.loaded { 50 | return f.err 51 | } 52 | f.loaded = true 53 | if f.realPath, f.err = realPath(f.path); f.err != nil { 54 | return f.err 55 | } 56 | 57 | return f.err 58 | } 59 | 60 | func (f *DefFile) Path() string { 61 | return f.path 62 | } 63 | 64 | func (f *DefFile) Exists() (bool, error) { 65 | if err := f.setup(); err != nil { 66 | return false, err 67 | } 68 | 69 | if _, err := os.Lstat(f.realPath); os.IsNotExist(err) { 70 | return false, nil 71 | } 72 | return true, nil 73 | } 74 | 75 | func (f *DefFile) Contains() (io.Reader, error) { 76 | if err := f.setup(); err != nil { 77 | return nil, err 78 | } 79 | 80 | fh, err := os.Open(f.realPath) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return fh, nil 85 | } 86 | 87 | func (f *DefFile) Mode() (string, error) { 88 | if err := f.setup(); err != nil { 89 | return "", err 90 | } 91 | 92 | fi, err := os.Lstat(f.realPath) 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | sys := fi.Sys() 98 | stat := sys.(*syscall.Stat_t) 99 | mode := fmt.Sprintf("%04o", (stat.Mode & 07777)) 100 | return mode, nil 101 | } 102 | 103 | func (f *DefFile) Size() (int, error) { 104 | if err := f.setup(); err != nil { 105 | return 0, err 106 | } 107 | 108 | fi, err := os.Lstat(f.realPath) 109 | if err != nil { 110 | return 0, err 111 | } 112 | 113 | size := fi.Size() 114 | return int(size), nil 115 | } 116 | 117 | func (f *DefFile) Filetype() (string, error) { 118 | if err := f.setup(); err != nil { 119 | return "", err 120 | } 121 | 122 | fi, err := os.Lstat(f.realPath) 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | switch { 128 | case fi.Mode()&os.ModeSymlink == os.ModeSymlink: 129 | return "symlink", nil 130 | case fi.IsDir(): 131 | return "directory", nil 132 | case fi.Mode().IsRegular(): 133 | return "file", nil 134 | } 135 | // FIXME: file as a catchall? 136 | return "file", nil 137 | } 138 | 139 | func (f *DefFile) Owner() (string, error) { 140 | if err := f.setup(); err != nil { 141 | return "", err 142 | } 143 | 144 | fi, err := os.Lstat(f.realPath) 145 | if err != nil { 146 | return "", err 147 | } 148 | 149 | uidS := fmt.Sprint(fi.Sys().(*syscall.Stat_t).Uid) 150 | uid, err := strconv.Atoi(uidS) 151 | if err != nil { 152 | return "", err 153 | } 154 | return getUserForUid(uid) 155 | } 156 | 157 | func (f *DefFile) Group() (string, error) { 158 | if err := f.setup(); err != nil { 159 | return "", err 160 | } 161 | 162 | fi, err := os.Lstat(f.realPath) 163 | if err != nil { 164 | return "", err 165 | } 166 | 167 | gidS := fmt.Sprint(fi.Sys().(*syscall.Stat_t).Gid) 168 | gid, err := strconv.Atoi(gidS) 169 | if err != nil { 170 | return "", err 171 | } 172 | return getGroupForGid(gid) 173 | } 174 | 175 | func (f *DefFile) LinkedTo() (string, error) { 176 | if err := f.setup(); err != nil { 177 | return "", err 178 | } 179 | 180 | dst, err := os.Readlink(f.realPath) 181 | if err != nil { 182 | return "", err 183 | } 184 | return dst, nil 185 | } 186 | 187 | func realPath(path string) (string, error) { 188 | if !strings.HasPrefix(path, "~") { 189 | return path, nil 190 | } 191 | pathS := strings.Split(path, "/") 192 | f := pathS[0] 193 | 194 | var usr user.User 195 | var err error 196 | if f == "~" { 197 | usr, err = user.CurrentUser() 198 | } else { 199 | usr, err = user.LookupUser(f[1:len(f)]) 200 | } 201 | if err != nil { 202 | return "", err 203 | } 204 | pathS[0] = usr.Home 205 | 206 | realPath := strings.Join(pathS, "/") 207 | realPath, err = filepath.Abs(realPath) 208 | 209 | return realPath, err 210 | } 211 | 212 | func (f *DefFile) Md5() (string, error) { 213 | 214 | if err := f.setup(); err != nil { 215 | return "", err 216 | } 217 | 218 | fh, err := os.Open(f.realPath) 219 | if err != nil { 220 | return "", err 221 | } 222 | defer fh.Close() 223 | 224 | hash := md5.New() 225 | if _, err := io.Copy(hash, fh); err != nil { 226 | return "", err 227 | } 228 | 229 | return fmt.Sprintf("%x", hash.Sum(nil)), nil 230 | } 231 | 232 | func (f *DefFile) Sha256() (string, error) { 233 | 234 | if err := f.setup(); err != nil { 235 | return "", err 236 | } 237 | 238 | fh, err := os.Open(f.realPath) 239 | if err != nil { 240 | return "", err 241 | } 242 | defer fh.Close() 243 | 244 | hash := sha256.New() 245 | if _, err := io.Copy(hash, fh); err != nil { 246 | return "", err 247 | } 248 | 249 | return fmt.Sprintf("%x", hash.Sum(nil)), nil 250 | } 251 | 252 | func getUserForUid(uid int) (string, error) { 253 | if user, err := user.LookupUid(uid); err == nil { 254 | return user.Name, nil 255 | } 256 | 257 | cmd := util.NewCommand("getent", "passwd", strconv.Itoa(uid)) 258 | if err := cmd.Run(); err != nil { 259 | return "", fmt.Errorf("Error: no matching entries in passwd file. getent passwd: %v", err) 260 | } 261 | userS := strings.Split(cmd.Stdout.String(), ":")[0] 262 | 263 | return userS, nil 264 | } 265 | 266 | func getGroupForGid(gid int) (string, error) { 267 | if group, err := user.LookupGid(gid); err == nil { 268 | return group.Name, nil 269 | } 270 | 271 | cmd := util.NewCommand("getent", "group", strconv.Itoa(gid)) 272 | if err := cmd.Run(); err != nil { 273 | return "", fmt.Errorf("Error: no matching entries in passwd file. getent group: %v", err) 274 | } 275 | groupS := strings.Split(cmd.Stdout.String(), ":")[0] 276 | 277 | return groupS, nil 278 | } 279 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "sort" 12 | "strings" 13 | 14 | "gopkg.in/yaml.v2" 15 | 16 | "github.com/aelsabbahy/goss/resource" 17 | "github.com/urfave/cli" 18 | ) 19 | 20 | const ( 21 | UNSET = iota 22 | JSON 23 | YAML 24 | ) 25 | 26 | var OutStoreFormat = UNSET 27 | var TemplateFilter func(data []byte) []byte 28 | var debug = false 29 | 30 | func getStoreFormatFromFileName(f string) int { 31 | ext := filepath.Ext(f) 32 | switch ext { 33 | case ".json": 34 | return JSON 35 | case ".yaml", ".yml": 36 | return YAML 37 | default: 38 | log.Fatalf("Unknown file extension: %v", ext) 39 | } 40 | return 0 41 | } 42 | 43 | func getStoreFormatFromData(data []byte) int { 44 | var v interface{} 45 | if err := unmarshalJSON(data, &v); err == nil { 46 | return JSON 47 | } 48 | if err := unmarshalYAML(data, &v); err == nil { 49 | return YAML 50 | } 51 | log.Fatalf("Unable to determine format from content") 52 | return 0 53 | } 54 | 55 | // Reads json file returning GossConfig 56 | func ReadJSON(filePath string) GossConfig { 57 | file, err := ioutil.ReadFile(filePath) 58 | if err != nil { 59 | fmt.Printf("File error: %v\n", err) 60 | os.Exit(1) 61 | } 62 | 63 | return ReadJSONData(file, false) 64 | } 65 | 66 | type TmplVars struct { 67 | Vars map[string]interface{} 68 | } 69 | 70 | func (t *TmplVars) Env() map[string]string { 71 | env := make(map[string]string) 72 | for _, i := range os.Environ() { 73 | sep := strings.Index(i, "=") 74 | env[i[0:sep]] = i[sep+1:] 75 | } 76 | return env 77 | } 78 | 79 | func varsFromFile(varsFile string) (map[string]interface{}, error) { 80 | var vars map[string]interface{} 81 | if varsFile == "" { 82 | return vars, nil 83 | } 84 | data, err := ioutil.ReadFile(varsFile) 85 | if err != nil { 86 | return vars, err 87 | } 88 | format := getStoreFormatFromData(data) 89 | if err := unmarshal(data, &vars, format); err != nil { 90 | return vars, err 91 | } 92 | return vars, nil 93 | } 94 | 95 | // Reads json byte array returning GossConfig 96 | func ReadJSONData(data []byte, detectFormat bool) GossConfig { 97 | if TemplateFilter != nil { 98 | data = TemplateFilter(data) 99 | if debug { 100 | fmt.Println("DEBUG: file after text/template render") 101 | fmt.Println(string(data)) 102 | } 103 | } 104 | format := OutStoreFormat 105 | if detectFormat == true { 106 | format = getStoreFormatFromData(data) 107 | } 108 | gossConfig := NewGossConfig() 109 | // Horrible, but will do for now 110 | if err := unmarshal(data, gossConfig, format); err != nil { 111 | // FIXME: really dude.. this is so ugly 112 | fmt.Printf("Error: %v\n", err) 113 | os.Exit(1) 114 | } 115 | return *gossConfig 116 | } 117 | 118 | // Reads json file recursively returning string 119 | func RenderJSON(c *cli.Context) string { 120 | filePath := c.GlobalString("gossfile") 121 | varsFile := c.GlobalString("vars") 122 | debug = c.Bool("debug") 123 | TemplateFilter = NewTemplateFilter(varsFile) 124 | path := filepath.Dir(filePath) 125 | OutStoreFormat = getStoreFormatFromFileName(filePath) 126 | gossConfig := mergeJSONData(ReadJSON(filePath), 0, path) 127 | 128 | b, err := marshal(gossConfig) 129 | if err != nil { 130 | log.Fatalf("Error rendering: %v\n", err) 131 | } 132 | return string(b) 133 | } 134 | 135 | func mergeJSONData(gossConfig GossConfig, depth int, path string) GossConfig { 136 | depth++ 137 | if depth >= 50 { 138 | fmt.Println("Error: Max depth of 50 reached, possibly due to dependency loop in goss file") 139 | os.Exit(1) 140 | } 141 | // Our return gossConfig 142 | ret := *NewGossConfig() 143 | ret = mergeGoss(ret, gossConfig) 144 | 145 | // Sort the gossfiles to ensure consistent ordering 146 | var keys []string 147 | for k, _ := range gossConfig.Gossfiles { 148 | keys = append(keys, k) 149 | } 150 | sort.Strings(keys) 151 | 152 | // Merge gossfiles in sorted order 153 | for _, k := range keys { 154 | g := gossConfig.Gossfiles[k] 155 | var fpath string 156 | if strings.HasPrefix(g.ID(), "/") { 157 | fpath = g.ID() 158 | } else { 159 | fpath = filepath.Join(path, g.ID()) 160 | } 161 | matches, err := filepath.Glob(fpath) 162 | if err != nil { 163 | fmt.Printf("Error in expanding glob pattern: \"%s\"\n", err.Error()) 164 | os.Exit(1) 165 | } 166 | for _, match := range matches { 167 | fdir := filepath.Dir(match) 168 | j := mergeJSONData(ReadJSON(match), depth, fdir) 169 | ret = mergeGoss(ret, j) 170 | } 171 | } 172 | return ret 173 | } 174 | 175 | func WriteJSON(filePath string, gossConfig GossConfig) error { 176 | jsonData, err := marshal(gossConfig) 177 | if err != nil { 178 | log.Fatalf("Error writing: %v\n", err) 179 | } 180 | 181 | // check if the auto added json data is empty before writing to file. 182 | emptyConfig := *NewGossConfig() 183 | emptyData, err := marshal(emptyConfig) 184 | if err != nil { 185 | log.Fatalf("Error writing: %v\n", err) 186 | } 187 | 188 | if string(emptyData) == string(jsonData) { 189 | log.Printf("Can't write empty configuration file. Please check resource name(s).") 190 | return nil 191 | } 192 | 193 | if err := ioutil.WriteFile(filePath, jsonData, 0644); err != nil { 194 | log.Fatalf("Error writing: %v\n", err) 195 | } 196 | 197 | return nil 198 | } 199 | 200 | func resourcePrint(fileName string, res resource.ResourceRead) { 201 | resMap := map[string]resource.ResourceRead{res.ID(): res} 202 | 203 | oj, _ := marshal(resMap) 204 | typ := reflect.TypeOf(res) 205 | typs := strings.Split(typ.String(), ".")[1] 206 | 207 | fmt.Printf("Adding %s to '%s':\n\n%s\n\n", typs, fileName, string(oj)) 208 | } 209 | 210 | func marshal(gossConfig interface{}) ([]byte, error) { 211 | switch OutStoreFormat { 212 | case JSON: 213 | return marshalJSON(gossConfig) 214 | case YAML: 215 | return marshalYAML(gossConfig) 216 | default: 217 | return nil, fmt.Errorf("StoreFormat unset") 218 | } 219 | } 220 | 221 | func unmarshal(data []byte, v interface{}, storeFormat int) error { 222 | switch storeFormat { 223 | case JSON: 224 | return unmarshalJSON(data, v) 225 | case YAML: 226 | return unmarshalYAML(data, v) 227 | default: 228 | return fmt.Errorf("StoreFormat unset") 229 | } 230 | } 231 | 232 | func marshalJSON(gossConfig interface{}) ([]byte, error) { 233 | return json.MarshalIndent(gossConfig, "", " ") 234 | } 235 | 236 | func unmarshalJSON(data []byte, v interface{}) error { 237 | return json.Unmarshal(data, v) 238 | } 239 | 240 | func marshalYAML(gossConfig interface{}) ([]byte, error) { 241 | return yaml.Marshal(gossConfig) 242 | } 243 | 244 | func unmarshalYAML(data []byte, v interface{}) error { 245 | return yaml.Unmarshal(data, v) 246 | } 247 | -------------------------------------------------------------------------------- /add.go: -------------------------------------------------------------------------------- 1 | package goss 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/aelsabbahy/goss/system" 11 | "github.com/aelsabbahy/goss/util" 12 | "github.com/urfave/cli" 13 | ) 14 | 15 | // Simple wrapper to add multiple resources 16 | func AddResources(fileName, resourceName string, keys []string, c *cli.Context) error { 17 | OutStoreFormat = getStoreFormatFromFileName(fileName) 18 | config := util.Config{ 19 | IgnoreList: c.GlobalStringSlice("exclude-attr"), 20 | Timeout: int(c.Duration("timeout") / time.Millisecond), 21 | AllowInsecure: c.Bool("insecure"), 22 | NoFollowRedirects: c.Bool("no-follow-redirects"), 23 | Server: c.String("server"), 24 | } 25 | 26 | var gossConfig GossConfig 27 | if _, err := os.Stat(fileName); err == nil { 28 | gossConfig = ReadJSON(fileName) 29 | } else { 30 | gossConfig = *NewGossConfig() 31 | } 32 | 33 | sys := system.New(c) 34 | 35 | for _, key := range keys { 36 | if err := AddResource(fileName, gossConfig, resourceName, key, c, config, sys); err != nil { 37 | return err 38 | } 39 | } 40 | WriteJSON(fileName, gossConfig) 41 | 42 | return nil 43 | } 44 | 45 | func AddResource(fileName string, gossConfig GossConfig, resourceName, key string, c *cli.Context, config util.Config, sys *system.System) error { 46 | // Need to figure out a good way to refactor this 47 | switch resourceName { 48 | case "Addr": 49 | res, err := gossConfig.Addrs.AppendSysResource(key, sys, config) 50 | if err != nil { 51 | fmt.Println(err) 52 | os.Exit(1) 53 | } 54 | resourcePrint(fileName, res) 55 | case "Command": 56 | res, err := gossConfig.Commands.AppendSysResource(key, sys, config) 57 | if err != nil { 58 | fmt.Println(err) 59 | os.Exit(1) 60 | } 61 | resourcePrint(fileName, res) 62 | case "DNS": 63 | res, err := gossConfig.DNS.AppendSysResource(key, sys, config) 64 | if err != nil { 65 | fmt.Println(err) 66 | os.Exit(1) 67 | } 68 | resourcePrint(fileName, res) 69 | case "File": 70 | res, err := gossConfig.Files.AppendSysResource(key, sys, config) 71 | if err != nil { 72 | fmt.Println(err) 73 | os.Exit(1) 74 | } 75 | resourcePrint(fileName, res) 76 | case "Group": 77 | res, err := gossConfig.Groups.AppendSysResource(key, sys, config) 78 | if err != nil { 79 | fmt.Println(err) 80 | os.Exit(1) 81 | } 82 | resourcePrint(fileName, res) 83 | case "Package": 84 | res, err := gossConfig.Packages.AppendSysResource(key, sys, config) 85 | if err != nil { 86 | fmt.Println(err) 87 | os.Exit(1) 88 | } 89 | resourcePrint(fileName, res) 90 | case "Port": 91 | res, err := gossConfig.Ports.AppendSysResource(key, sys, config) 92 | if err != nil { 93 | fmt.Println(err) 94 | os.Exit(1) 95 | } 96 | resourcePrint(fileName, res) 97 | case "Process": 98 | res, err := gossConfig.Processes.AppendSysResource(key, sys, config) 99 | if err != nil { 100 | fmt.Println(err) 101 | os.Exit(1) 102 | } 103 | resourcePrint(fileName, res) 104 | case "Service": 105 | res, err := gossConfig.Services.AppendSysResource(key, sys, config) 106 | if err != nil { 107 | fmt.Println(err) 108 | os.Exit(1) 109 | } 110 | resourcePrint(fileName, res) 111 | case "User": 112 | res, err := gossConfig.Users.AppendSysResource(key, sys, config) 113 | if err != nil { 114 | fmt.Println(err) 115 | os.Exit(1) 116 | } 117 | resourcePrint(fileName, res) 118 | case "Gossfile": 119 | res, err := gossConfig.Gossfiles.AppendSysResource(key, sys, config) 120 | if err != nil { 121 | fmt.Println(err) 122 | os.Exit(1) 123 | } 124 | resourcePrint(fileName, res) 125 | case "KernelParam": 126 | res, err := gossConfig.KernelParams.AppendSysResource(key, sys, config) 127 | if err != nil { 128 | fmt.Println(err) 129 | os.Exit(1) 130 | } 131 | resourcePrint(fileName, res) 132 | case "Mount": 133 | res, err := gossConfig.Mounts.AppendSysResource(key, sys, config) 134 | if err != nil { 135 | fmt.Println(err) 136 | os.Exit(1) 137 | } 138 | resourcePrint(fileName, res) 139 | case "Interface": 140 | res, err := gossConfig.Interfaces.AppendSysResource(key, sys, config) 141 | if err != nil { 142 | fmt.Println(err) 143 | os.Exit(1) 144 | } 145 | resourcePrint(fileName, res) 146 | case "HTTP": 147 | res, err := gossConfig.HTTPs.AppendSysResource(key, sys, config) 148 | if err != nil { 149 | fmt.Println(err) 150 | os.Exit(1) 151 | } 152 | resourcePrint(fileName, res) 153 | default: 154 | panic("Undefined resource name: " + resourceName) 155 | } 156 | 157 | return nil 158 | } 159 | 160 | // Simple wrapper to add multiple resources 161 | func AutoAddResources(fileName string, keys []string, c *cli.Context) error { 162 | OutStoreFormat = getStoreFormatFromFileName(fileName) 163 | config := util.Config{ 164 | IgnoreList: c.GlobalStringSlice("exclude-attr"), 165 | Timeout: int(c.Duration("timeout") / time.Millisecond), 166 | } 167 | 168 | var gossConfig GossConfig 169 | if _, err := os.Stat(fileName); err == nil { 170 | gossConfig = ReadJSON(fileName) 171 | } else { 172 | gossConfig = *NewGossConfig() 173 | } 174 | 175 | sys := system.New(c) 176 | 177 | for _, key := range keys { 178 | if err := AutoAddResource(fileName, gossConfig, key, c, config, sys); err != nil { 179 | return err 180 | } 181 | } 182 | WriteJSON(fileName, gossConfig) 183 | 184 | return nil 185 | } 186 | 187 | func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *cli.Context, config util.Config, sys *system.System) error { 188 | // file 189 | if strings.Contains(key, "/") { 190 | if res, _, ok := gossConfig.Files.AppendSysResourceIfExists(key, sys); ok == true { 191 | resourcePrint(fileName, res) 192 | } 193 | } 194 | 195 | // group 196 | if res, _, ok := gossConfig.Groups.AppendSysResourceIfExists(key, sys); ok == true { 197 | resourcePrint(fileName, res) 198 | } 199 | 200 | // package 201 | if res, _, ok := gossConfig.Packages.AppendSysResourceIfExists(key, sys); ok == true { 202 | resourcePrint(fileName, res) 203 | } 204 | 205 | // port 206 | if res, _, ok := gossConfig.Ports.AppendSysResourceIfExists(key, sys); ok == true { 207 | resourcePrint(fileName, res) 208 | } 209 | 210 | // process 211 | if res, sysres, ok := gossConfig.Processes.AppendSysResourceIfExists(key, sys); ok == true { 212 | resourcePrint(fileName, res) 213 | ports := system.GetPorts(true) 214 | pids, _ := sysres.Pids() 215 | for _, pid := range pids { 216 | pidS := strconv.Itoa(pid) 217 | for port, entries := range ports { 218 | for _, entry := range entries { 219 | if entry.Pid == pidS { 220 | // port 221 | if res, _, ok := gossConfig.Ports.AppendSysResourceIfExists(port, sys); ok == true { 222 | resourcePrint(fileName, res) 223 | } 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | // Service 231 | if res, _, ok := gossConfig.Services.AppendSysResourceIfExists(key, sys); ok == true { 232 | resourcePrint(fileName, res) 233 | } 234 | 235 | // user 236 | if res, _, ok := gossConfig.Users.AppendSysResourceIfExists(key, sys); ok == true { 237 | resourcePrint(fileName, res) 238 | } 239 | 240 | return nil 241 | } 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goss - Quick and Easy server validation 2 | [![Build Status](https://travis-ci.org/aelsabbahy/goss.svg?branch=master)](https://travis-ci.org/aelsabbahy/goss) 3 | [![Github All Releases](https://img.shields.io/github/downloads/aelsabbahy/goss/total.svg?maxAge=604800)](https://github.com/aelsabbahy/goss/releases) 4 | ** 5 | [![Twitter Follow](https://img.shields.io/twitter/follow/aelsabbahy1.svg?style=social&label=Follow&maxAge=2592000)]() 6 | [![Blog](https://img.shields.io/badge/follow-blog-brightgreen.svg)](https://medium.com/@aelsabbahy) 7 | 8 | ## Goss in 45 seconds 9 | 10 | **Note:** For an even faster way of doing this, see: [autoadd](https://github.com/aelsabbahy/goss/blob/master/docs/manual.md#autoadd-aa---auto-add-all-matching-resources-to-test-suite) 11 | 12 | **Note:** For testing docker containers see the [dgoss](https://github.com/aelsabbahy/goss/tree/master/extras/dgoss) wrapper 13 | 14 | **Note:** For some Docker/Kubernetes healtcheck, health endpoint, and container ordering examples, see my blog post [here](https://medium.com/@aelsabbahy/docker-1-12-kubernetes-simplified-health-checks-and-container-ordering-with-goss-fa8debbe676c) 15 | 16 | asciicast 17 | 18 | ## Introduction 19 | 20 | ### What is Goss? 21 | 22 | Goss is a YAML based [serverspec](http://serverspec.org/) alternative tool for validating a server’s configuration. It eases the process of writing tests by allowing the user to generate tests from the current system state. Once the test suite is written they can be executed, waited-on, or served as a health endpoint. 23 | 24 | ### Why use Goss? 25 | 26 | * Goss is EASY! - [Goss in 45 seconds](#goss-in-45-seconds) 27 | * Goss is FAST! - small-medium test suits are near instantaneous, see [benchmarks](https://github.com/aelsabbahy/goss/wiki/Benchmarks) 28 | * Goss is SMALL! - <10MB single self-contained binary 29 | 30 | ## Installation 31 | 32 | This will install goss and [dgoss](https://github.com/aelsabbahy/goss/tree/master/extras/dgoss). 33 | 34 | **Note:** Using `curl | sh` is not recommended for production systems, use manual installation below. 35 | 36 | ```bash 37 | # Install latest version to /usr/local/bin 38 | curl -fsSL https://goss.rocks/install | sh 39 | 40 | # Install v0.3.3 version to ~/bin 41 | curl -fsSL https://goss.rocks/install | GOSS_VER=v0.3.3 GOSS_DST=~/bin sh 42 | ``` 43 | 44 | ### Manual installation 45 | ```bash 46 | # See https://github.com/aelsabbahy/goss/releases for release versions 47 | curl -L https://github.com/aelsabbahy/goss/releases/download/_VERSION_/goss-linux-amd64 -o /usr/local/bin/goss 48 | chmod +rx /usr/local/bin/goss 49 | 50 | # (optional) dgoss docker wrapper (use 'master' for latest version) 51 | curl -L https://raw.githubusercontent.com/aelsabbahy/goss/_VERSION_/extras/dgoss/dgoss -o /usr/local/bin/dgoss 52 | chmod +rx /usr/local/bin/dgoss 53 | ``` 54 | 55 | ## Full Documentation 56 | 57 | Documentation is available here: https://github.com/aelsabbahy/goss/blob/master/docs/manual.md 58 | 59 | ## Quick start 60 | 61 | ### Writing a simple sshd test 62 | 63 | An initial set of tests can be derived from the system state by using the [add](https://github.com/aelsabbahy/goss/blob/master/docs/manual.md#add-a---add-system-resource-to-test-suite) or [autoadd](https://github.com/aelsabbahy/goss/blob/master/docs/manual.md#autoadd-aa---auto-add-all-matching-resources-to-test-suite) commands. 64 | 65 | Let's write a simple sshd test using autoadd. 66 | 67 | ``` 68 | # Running it as root will allow it to also detect ports 69 | $ sudo goss autoadd sshd 70 | ``` 71 | Generated `goss.yaml`: 72 | ```yaml 73 | $ cat goss.yaml 74 | port: 75 | tcp:22: 76 | listening: true 77 | ip: 78 | - 0.0.0.0 79 | tcp6:22: 80 | listening: true 81 | ip: 82 | - '::' 83 | service: 84 | sshd: 85 | enabled: true 86 | running: true 87 | user: 88 | sshd: 89 | exists: true 90 | uid: 74 91 | gid: 74 92 | groups: 93 | - sshd 94 | home: /var/empty/sshd 95 | shell: /sbin/nologin 96 | group: 97 | sshd: 98 | exists: true 99 | gid: 74 100 | process: 101 | sshd: 102 | running: true 103 | ``` 104 | Now that we have a test suite, we can: 105 | 106 | * Run it once 107 | ``` 108 | goss validate 109 | ............... 110 | 111 | Total Duration: 0.021s # <- yeah, it's that fast.. 112 | Count: 15, Failed: 0 113 | 114 | ``` 115 | * Edit it to use [templates](https://github.com/aelsabbahy/goss/blob/master/docs/manual.md#templates), and run with a vars file 116 | ``` 117 | goss --vars vars.yaml validate 118 | ``` 119 | 120 | * keep running it until the system enters a valid state or we timeout 121 | ``` 122 | goss validate --retry-timeout 30s --sleep 1s 123 | ``` 124 | * serve the tests as a health endpoint 125 | ``` 126 | goss serve & 127 | curl localhost:8080/healthz 128 | 129 | # JSON endpoint 130 | goss serve --format json & 131 | curl localhost:8080/healthz 132 | ``` 133 | 134 | ### Manually editing Goss files 135 | Goss files can be manually edited to use: 136 | * [Patterns](https://github.com/aelsabbahy/goss/blob/master/docs/manual.md#patterns) 137 | * [Advanced Matchers](https://github.com/aelsabbahy/goss/blob/master/docs/manual.md#advanced-matchers) 138 | * [Templates](https://github.com/aelsabbahy/goss/blob/master/docs/manual.md#templates) 139 | * `title` and `meta` (arbitrary data) attributes are persisted when adding other resources with `goss add` 140 | 141 | Some examples: 142 | ```yaml 143 | user: 144 | sshd: 145 | title: UID must be between 50-100, GID doesn't matter. home is flexible 146 | meta: 147 | desc: Ensure sshd is enabled and running since it's needed for system management 148 | sev: 5 149 | exists: true 150 | uid: 151 | # Validate that UID is between 50 and 100 152 | and: 153 | gt: 50 154 | lt: 100 155 | home: 156 | # Home can be any of the following 157 | or: 158 | - /var/empty/sshd 159 | - /var/run/sshd 160 | 161 | package: 162 | kernel: 163 | installed: true 164 | versions: 165 | # Must have 3 kernels and none of them can be 4.4.0 166 | and: 167 | - have-len: 3 168 | - not: 169 | contain-element: 4.4.0 170 | 171 | # Loaded from --vars YAML/JSON file 172 | {{.Vars.package}}: 173 | installed: true 174 | 175 | {{if eq .Env.OS "centos"}} 176 | # This test is only when $OS environment variable is set to "centos" 177 | libselinux: 178 | installed: true 179 | {{end}} 180 | ``` 181 | 182 | ## Supported resources 183 | * package - add new package 184 | * file - add new file 185 | * addr - add new remote address:port - ex: google.com:80 186 | * port - add new listening [protocol]:port - ex: 80 or udp:123 187 | * service - add new service 188 | * user - add new user 189 | * group - add new group 190 | * command - add new command 191 | * dns - add new dns 192 | * process - add new process name 193 | * kernel-param - add new kernel-param 194 | * mount - add new mount 195 | * interface - add new network interface 196 | * http - add new network http url 197 | * goss - add new goss file, it will be imported from this one 198 | 199 | ## Supported output formats 200 | * rspecish **(default)** - Similar to rspec output 201 | * documentation - Verbose test results 202 | * JSON - Detailed test result 203 | * TAP 204 | * JUnit 205 | * nagios - Nagios/Sensu compatible output /w exit code 2 for failures. 206 | * nagios_verbose - nagios output with verbose failure output. 207 | * silent - No output. Avoids exposing system information (e.g. when serving tests as a healthcheck endpoint). 208 | 209 | ## Community Contributions 210 | * [goss-ansible](https://github.com/indusbox/goss-ansible) - Ansible module for Goss. 211 | * [degoss](https://github.com/naftulikay/ansible-role-degoss) - Ansible role for installing, running, and removing Goss in a single go. 212 | * [kitchen-goss](https://github.com/ahelal/kitchen-goss) - A test-kitchen verifier plugin for Goss. 213 | * [goss-fpm-files](https://github.com/deanwilson/unixdaemon-fpm-cookery-recipes) - Might be useful for building goss system packages. 214 | * [molecule](https://github.com/metacloud/molecule) - Automated testing for Ansible roles, with native Goss support. 215 | * [packer-provisioner-goss](https://github.com/YaleUniversity/packer-provisioner-goss) - A packer plugin to run Goss as a provision step. 216 | 217 | ## Limitations 218 | 219 | Currently goss only runs on Linux. 220 | 221 | The following tests have limitations. 222 | 223 | Package: 224 | * rpm 225 | * deb 226 | * Alpine apk 227 | * pacman 228 | 229 | Service: 230 | * systemd 231 | * sysV init 232 | * OpenRC init 233 | * Upstart 234 | --------------------------------------------------------------------------------