diff --git a/.travis.yml b/.travis.yml index be5e022c..6409b67c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,8 +41,8 @@ deploy: api_key: secure: ijNltjw/mIHIOx8vLZ6asUun3SbY7D+XZbs5NX8vcIv0jvOiwaaT1hqny7SQBHfGZzqHsYUSS/GYAYJdBqKFFfGmTZsl90hFT6D0RGdz9C71UVxNFX4wQ5KQ/WVvdMT2SrLymGvu9TvoU0VG8OWqWVdxSlUPf6qOTGAagrzg+Tbsbb6czeiG67mlBBL23XSlfMG1p45UxzvI41SZj2R3ElUb0hym1CrFaoC36PBGrb0x41TXzvd8J7cu6xDzgczYhnYQQZpS6f2YcqNV1z0f+P67EQqQiDWIIcK2jE/YG+RgM8cbpLMiMec8CDiwNCsejBA5EbVMlGJlODvBXT5NmMBeugueqfSHEfkl5qZTQG4AOAT7UsqbnM7r0NqzmaE5Lj90igvJK6rNsH1ZRe79WfSsTtuzlkkouHGvyoz0M8gnMSzpbbwoyIy+UT0hhPMoZvIpXfr43en5WkbkPKfop0p4Vjc8NGg0iD45q1JAvIVTtz/WvWTknM1P8e3u+TiDTaZkcJJmFaBqgaeLoWktOGfi54p9nhgQnSyBYt4PyvhWDQs7QFmX0BdKlqJCESvUOJTe1t6zJJsV7Gn/3sGCN7JUEwbnXTsCoMjjFFUvQdm0Ur7t7/2xU3kO+dyfqcdM/5SYFeppQcjHI0ckhI51mIoBTsJsGvaVwKKL1I4cyBU= file: - - release/goss-alpha-darwin-amd64 - - release/goss-alpha-darwin-amd64.sha256 + - release/goss-darwin-amd64 + - release/goss-darwin-amd64.sha256 - release/goss-linux-amd64 - release/goss-linux-amd64.sha256 - release/goss-linux-386 diff --git a/Makefile b/Makefile index 01fa73b9..5ddfb3d5 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,20 @@ install: release/goss-linux-amd64 test: $(info INFO: Starting build $@) - ./ci/go-test.sh $(pkgs) + ./ci/go-test.sh + +cov: + go test -coverpkg=./... -coverprofile=c.out ./... + # go tool cover -func ./c.out + +funcov: + go test -coverpkg=./... -coverprofile=c.out ./... + go tool cover -func ./c.out + +htmlcov: + go test -v -coverpkg=./... -coverprofile=c.out ./... + go tool cover -html ./c.out + lint: $(info INFO: Starting build $@) @@ -36,15 +49,13 @@ bench: $(info INFO: Starting build $@) go test -bench=. -alpha-test-%: release/goss-% +test-int-validate-%: release/goss-% $(info INFO: Starting build $@) - ./integration-tests/run-tests-alpha.sh $* + ./integration-tests/run-validate-tests.sh $* test-int-serve-%: release/goss-% $(info INFO: Starting build $@) ./integration-tests/run-serve-tests.sh $* -# shim to account for linux being not in alpha -test-int-serve-linux-amd64: test-int-serve-alpha-linux-amd64 release/goss-%: $(GO_FILES) ./release-build.sh $* @@ -71,6 +82,10 @@ push-images: $(info INFO: Starting build $@) development/push_images.sh +# Update the matcher test golden files +update-matcher-tests: + go test -v -run '^TestMatchers' . -update + test-darwin-all: test-short-all test-int-darwin-all # linux _does_ have the docker-style testing, but does _not_ currently have the same style integration tests darwin+windows do, _because_ of the docker-style testing. test-linux-all: test-short-all test-int-64 test-int-32 @@ -78,8 +93,8 @@ test-windows-all: test-short-all test-int-windows-all test-int-64: centos7 wheezy trusty alpine3 arch test-int-serve-linux-amd64 test-int-32: centos7-32 wheezy-32 trusty-32 alpine3-32 arch-32 -test-int-darwin-all: alpha-test-alpha-darwin-amd64 test-int-serve-alpha-darwin-amd64 -test-int-windows-all: alpha-test-alpha-windows-amd64 test-int-serve-alpha-windows-amd64 +test-int-darwin-all: test-int-validate-darwin-amd64 test-int-serve-darwin-amd64 +test-int-windows-all: test-int-validate-windows-amd64 test-int-serve-windows-amd64 test-int-all: test-int-32 test-int-64 centos7-32: build diff --git a/README.md b/README.md index 23822927..dc03e750 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ asciicast -**Note:** For an even faster way of doing this, see: [autoadd](https://github.com/goss-org/goss/blob/master/docs/manual.md#autoadd-aa---auto-add-all-matching-resources-to-test-suite) - **Note:** For testing docker containers see the [dgoss](https://github.com/goss-org/goss/tree/master/extras/dgoss) wrapper. Also, user submitted wrapper scripts for Kubernetes [kgoss](https://github.com/goss-org/goss/tree/master/extras/kgoss) and Docker Compose [dcgoss](https://github.com/goss-org/goss/tree/master/extras/dcgoss). **Note:** For some Docker/Kubernetes healthcheck, health endpoint, and diff --git a/ci/go-test.sh b/ci/go-test.sh index e715f991..d0623f91 100755 --- a/ci/go-test.sh +++ b/ci/go-test.sh @@ -3,7 +3,7 @@ set -euo pipefail command -v go -go test -coverprofile="c.out" "${1}" +go test -coverpkg=./... ./... -skip '^TestPrometheus' -coverprofile="c.out" sed 's|github.com/goss-org/goss/||' <"c.out" >"c.out.tmp" diff --git a/cmd/goss/goss.go b/cmd/goss/goss.go index f720fc76..203ac1e8 100644 --- a/cmd/goss/goss.go +++ b/cmd/goss/goss.go @@ -67,7 +67,6 @@ func timeoutFlag(value time.Duration) cli.DurationFlag { } func main() { - startTime := time.Now() app := cli.NewApp() app.EnableBashCompletion = true app.Version = version @@ -149,7 +148,7 @@ func main() { }, Action: func(c *cli.Context) error { fatalAlphaIfNeeded(c) - code, err := goss.Validate(newRuntimeConfigFromCLI(c), startTime) + code, err := goss.Validate(newRuntimeConfigFromCLI(c)) if err != nil { color.Red(fmt.Sprintf("Error: %v\n", err)) } @@ -416,14 +415,14 @@ func addAlphaFlagIfNeeded(app *cli.App) { if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { app.Flags = append(app.Flags, cli.StringFlag{ Name: "use-alpha", - Usage: fmt.Sprintf("goss is alpha-quality. Set to 1 to use anyway."), + Usage: fmt.Sprintf("goss on macOS/Windows is alpha-quality. Set to 1 to use anyway."), EnvVar: "GOSS_USE_ALPHA", Value: "0", }) } } -const msgFormat string = `WARNING: goss for this platform (%q) is alpha-quality, work-in-progress, and not yet exercised within continuous integration. +const msgFormat string = `WARNING: goss for this platform (%q) is alpha-quality, work-in-progress and community-supported. You should not expect everything to work. Treat linux as the canonical behaviour to expect. diff --git a/docs/manual.md b/docs/manual.md index c339a8f8..8b201c63 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -4,59 +4,6 @@ ## Table of Contents -- [goss manual](#goss-manual) - - [Table of Contents](#table-of-contents) - - [Usage](#usage) - - [global options](#global-options) - - [--gossfile gossfile | -g gossfile](#--gossfile-gossfile-or--g-gossfile) - - [--vars](#--vars) - - [--package ](#--package-type) - - [commands](#commands) - - [add, a - Add system resource to test suite](#add-a---add-system-resource-to-test-suite) - - [Resource types](#resource-types) - - [Flags](#flags) - - [--exclude-attr](#--exclude-attr) - - [Example:](#example) - - [autoadd, aa - Auto add all matching resources to test suite](#autoadd-aa---auto-add-all-matching-resources-to-test-suite) - - [Example:](#example-1) - - [render, r - Render gossfile after importing all referenced gossfiles](#render-r---render-gossfile-after-importing-all-referenced-gossfiles) - - [Flags](#flags-1) - - [--debug](#--debug) - - [Example:](#example-2) - - [serve, s - Serve a health endpoint](#serve-s---serve-a-health-endpoint) - - [Flags](#flags-2) - - [Example:](#example-3) - - [validate, v - Validate the system](#validate-v---validate-the-system) - - [Flags](#flags-3) - - [Examples:](#examples) - - [Goss test creation](#goss-test-creation) - - [Important note about goss file format](#important-note-about-goss-file-format) - - [Available tests](#available-tests) - - [addr](#addr) - - [command](#command) - - [dns](#dns) - - [file](#file) - - [gossfile](#gossfile) - - [group](#group) - - [http](#http) - - [interface](#interface) - - [kernel-param](#kernel-param) - - [mount](#mount) - - [matching](#matching) - - [With Templates:](#with-templates) - - [Without Templates:](#without-templates) - - [package](#package) - - [port](#port) - - [process](#process) - - [service](#service) - - [user](#user) - - [Patterns](#patterns) - - [Example](#example-4) - - [Advanced Matchers](#advanced-matchers) - - [Examples](#examples-1) - - [Templates](#templates) - - [Examples](#examples-2) - ## Usage ``` @@ -321,8 +268,9 @@ The `application/vnd.goss-{output format}` media type can be used in the `Accept * `silent` - No output. Avoids exposing system information (e.g. when serving tests as a healthcheck endpoint) * `--format-options`, `-o` (output format option) * `perfdata` - Outputs Nagios "performance data". Applies to `nagios` output - * `verbose` - Gives verbose output. Applies to `nagios` output - * `pretty` - Pretty printing for the `json` output + * `verbose` - Gives verbose output. Applies to `nagios` output + * `pretty` - Pretty printing for the `json` output + * `sort` - Sorts the results * `--loglevel level`, `-L level` - Goss logging verbosity level (default: `INFO`). `level` can be one of `TRACE | DEBUG | INFO | WARN | ERROR | FATAL`. Lower levels of tracing include all upper levels traces also (ie. INFO include WARN, ERROR and FATAL outputs). * `TRACE` - Print details for each check, successful or not and all incoming healthchecks * `DEBUG` - Print details of summary response to healthchecks including remote IP address, return code and full body @@ -488,24 +436,27 @@ Validates if a remote `address:port` are accessible. ```yaml addr: tcp://ip-address-or-domain-name:80: + # required attributes reachable: true - timeout: 500 # optional attributes + # defaults to hash key + address: "tcp://ip-address-or-domain-name:80" + timeout: 500 local-address: 127.0.0.1 ``` ### command -Validates the exit-status and output of a command +Validates the exit-status and output of a command. This can be used in combination with the [gjson](#gjson) matcher to create powerful goss custom tests. ```yaml command: - version: + 'go version': # required attributes exit-status: 0 + # optional attributes # defaults to hash key exec: "go version" - # optional attributes stdout: - go version go1.6 linux/amd64 stderr: [] @@ -527,6 +478,8 @@ dns: # required attributes resolvable: true # optional attributes + # defaults to hash key + resolve: localhost addrs: - 127.0.0.1 - ::1 @@ -593,12 +546,14 @@ file: # required attributes exists: true # optional attributes + # defaults to hash key + path: /etc/passwd mode: "0644" size: 2118 # in bytes owner: root group: root filetype: file # file, symlink, directory - contains: [] # Check file content for these patterns + contents: [] # Check file content for these patterns md5: 7c9bb14b3bf178e82c00c2a4398c93cd # md5 checksum of file # A stronger checksum alternatives to md5 (recommended) sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c @@ -612,7 +567,7 @@ file: skip: false ``` -`contains` can be a string or a [pattern](#patterns) +`contents` can be a string or a [pattern](#patterns) ### gossfile @@ -642,6 +597,8 @@ group: # required attributes exists: true # optional attributes + # defaults to hash key + groupname: /etc/passwd gid: 65534 skip: false ``` @@ -656,6 +613,8 @@ http: # required attributes status: 200 # optional attributes + # defaults to hash key + url: https://www.google.com allow-insecure: false no-follow-redirects: false # Setting this to true will NOT follow redirects timeout: 1000 @@ -685,6 +644,8 @@ interface: # required attributes exists: true # optional attributes + # defaults to hash key + name: eth0 addrs: - 172.17.0.2/16 - fe80::42:acff:fe11:2/64 @@ -700,6 +661,9 @@ kernel-param: kernel.ostype: # required attributes value: Linux + # optional attributes + # defaults to hash key + name: kernel.ostype ``` To see the full list of current values, run `sysctl -a`. @@ -714,6 +678,8 @@ mount: # required attributes exists: true # optional attributes + # defaults to hash key + mountpoint: /home opts: - rw - relatime @@ -776,8 +742,6 @@ matching: baz: bing matches: and: - - have-key-with-value: - foo: bar - have-key: baz ``` @@ -790,6 +754,8 @@ package: # required attributes installed: true # optional attributes + # defaults to hash key + name: httpd versions: - 2.2.15 skip: false @@ -810,6 +776,8 @@ port: # required attributes listening: true # optional attributes + # defaults to hash key + port: 'tcp:22' ip: # what IP(s) is it listening on - 0.0.0.0 skip: false @@ -824,10 +792,13 @@ process: chrome: # required attributes running: true + # optional attributes + # defaults to hash key + comm: chrome skip: false ``` -**NOTE:** This check is inspecting the name of the binary, not the name of the process. For example, a process with the name `nginx: master process /usr/sbin/nginx` would be checked with the process `nginx`. To discover the binary of a pid run `ps -p -o comm`. +**NOTE:** This check is inspecting the name of the binary, not the name of the process. For example, a process with the name `nginx: master process /usr/sbin/nginx` would be checked with the process `nginx`. To discover the binary of a pid run `cat -E /proc//comm`. ### service Validates the state of a service. @@ -835,12 +806,17 @@ Validates the state of a service. ```yaml service: sshd: - # required attributes + # Optional attributes + # defaults to hash key + name: sshd enabled: true running: true + runlevels: ["3", "4", "5"] # Alpine example, runlevels: ["default"] skip: false ``` +`runlevels` is only supported on Alpine init, sysv init, and upstart + **NOTE:** this will **not** automatically check if the process is alive, it will check the status from `systemd`/`upstart`/`init`. @@ -853,6 +829,8 @@ user: # required attributes exists: true # optional attributes + # defaults to hash key + username: nfsnobody uid: 65534 gid: 65534 groups: @@ -865,91 +843,293 @@ user: **NOTE:** This check is inspecting the contents of local passwd file `/etc/passwd`, this does not validate remote users (e.g. LDAP). -## Patterns -For the attributes that use patterns (ex. `file`, `command` `output`), each pattern is checked against the attribute string, the type of patterns are: +## Matchers + +### Default Matchers + +Default matchers are determined by the attribute value received from the system. + +#### Bool, Strings, Integers + +Bool, Strings and integers are compared using equality, for example: + +```yaml +matching: + basic_string: + content: 'foo' + matches: 'foo' + +user: + nfsnobody: + exists: true + uid: 65534 +``` + +#### Arrays + +Arrays are treated as a [contains-elements](#array-matchers) by default, this validates that the expected test is a subset of the returned system state. + +```yaml +matching: + basic_array: + content: + - 'group1' + - 'group2' + - 'group3' + matches: + - 'group1' + - 'group2' + + # This fails, since the returned result and it's no longer a subset + basic_array_failing: + content: + - 'group1' + - 'group2' + - 'group3' + matches: + - 'group1' + - 'group2' + - 'group2' # this 2nd group2 is not in the returned content +``` + +#### io.Readers -* `"string"` - checks if any line contain string. -* `"!string"` - inverse of above, checks that no line contains string -* `"\\!string"` - escape sequence, check if any line contains `"!string"` -* `"/regex/"` - verifies that line contains regex -* `"!/regex/"` - inverse of above, checks that no line contains regex +This is the most magical matcher for goss. It remains a default for historic and performance reasons. Some attributes return an io.Reader that is read line by line (ex. file content, command, http body). This allows goss to validate large files/content efficiently. -**NOTE:** Pattern attributes do not support [Advanced Matchers](#advanced-matchers) -**NOTE:** Regex support is based on golang's regex engine documented [here](https://golang.org/pkg/regexp/syntax/) +Each pattern is checked against the attribute output, the type of patterns are: + +* `"foo"` - checks if any line contains `foo` +* `"!foo"` - inverse of above, checks that no line contains `foo` +* `"\\!foo"` - escape sequence, check if any line contains `!string` +* `"/[Rr]egex/"` - verifies that line matches regex +* `"!/[Rr]egex/"` - inverse of above, checks that no line matches regex + +**NOTE:** Regex support is based on Golang's regex engine documented [here](https://golang.org/pkg/regexp/syntax/) **NOTE:** You will **need** the double backslash (`\\`) escape for Regex special entities, for example `\\s` for blank spaces. -### Example -```bash -$ cat /tmp/test.txt -found -!alsofound +Example: + +```yaml +file: + /tmp/test.txt: + exists: true + contents: + - "foo" + - "!bar" + - "/[Gg]oss/" +``` +The above can be expressed as: -$ cat goss.yaml +```yaml file: /tmp/test.txt: exists: true - contains: - - found - - /fou.d/ - - "\\!alsofound" - - "!missing" - - "!/mis.ing/" + contents: + and: + - contain-element: "foo" + - not: {contain-element: "bar"} + - contain-element: {match-regexp: "[Gg]oss"} + +``` + +### Transforms + +If the system state type and the expected type don't match, goss will attempt to transform the system state type before matching it. -$ goss validate -.. +For example, kernel-param attribute returns a string, however, it can be tested using numeric comparisons: + +Example kernel-param test: +```yaml +kernel-param: + net.core.somaxconn: + value: "128" +``` + +Example (failing) kernel-param test with transform: +```yaml +kernel-param: + net.core.somaxconn: + value: {gt: 200} +``` + +When a transformed test fails, it will detail the transformers used, the `-o include_raw` option can be used to include the raw, untransformed attribute value: +``` +$ goss v +F + +Failures/Skipped: + +KernelParam: net.core.somaxconn: value: +Expected + 128 +to be > + 200 +the transform chain was + [{"to-numeric":{}}] + +Total Duration: 0.001s +Count: 1, Failed: 1, Skipped: 0 + + +$ goss v -o include_raw +F + +Failures/Skipped: + +KernelParam: net.core.somaxconn: value: +Expected + 128 +to be > + 200 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "128" Total Duration: 0.001s -Count: 2, Failed: 0 +Count: 1, Failed: 1, Skipped: 0 + ``` -## Advanced Matchers -Goss supports advanced matchers by converting json input to [gomega](https://onsi.github.io/gomega/) matchers. +### Advanced Matchers -### Examples +Goss supports advanced matchers by converting YAML input to [gomega](https://onsi.github.io/gomega/) matchers. -Validate that user `nobody` has a `uid` that is less than `500` and that they are **only** a member of the `nobody` group. +#### String Matchers +These will convert the system attribute to a string prior to matching. + +* `'55'` - Checks that the numeric is "55" when converted to string +* `have-prefix: pre` - Checks if string starts with "pre" +* `have-suffix: suf` - Checks if string ends with "suf" +* `match-regexp: '.*'` - Checks if string matches regexp +* `contain-substring: '2'` - Checks if string contains "2" + +Example: ```yaml -user: - nobody: - exists: true - uid: - lt: 500 - groups: - consist-of: [nobody] +matching: + example: + content: 42 + matches: + and: + - '42' + - have-prefix: '4' + - have-suffix: '2' + - match-regexp: '\d{2}' + - contain-substring: '2' ``` -Matchers can be nested for more complex logic, for example you can ensure that you have 3 kernel versions installed and none of them are `4.1.0`: +#### Numeric matchers +These will convert the system attribute to a numeric prior to matching. + +* `42` - If the expected type is a number +* `gt, ge, lt, le` - Greater than, greater than or equal, less than, etc.. + +Example: ```yaml -package: - kernel: - installed: true - versions: +matching: + example: + content: "42" + matches: and: - - have-len: 3 - - not: - contain-element: "4.1.0" + - 42 + - 42.0 + - gt: 40 + - lt: 45 ``` -Custom semver matcher is available under `semver-constraint`: +#### Array matchers + +These will convert the system attribute to an array prior to matching. Strings are split on "\n" + +* `contain-element: matcher` - Checks if the array contains an element that passes the matcher +* `contain-elements: [matcher, ...]` - checks if the array is a superset of the provided matchers +* `[matcher, ...]` - same as above +* `equal: [value, ...]` - Checks if the array is exactly equal to provided array +* `consist-of: [matcher, ...]` - Checks if the array consists of the provided matchers (order does not matter) + +Example: ```yaml -example: - content: - - 1.0.1 - - 1.9.9 - matches: - semver-constraint: ">1.0.0 <2.0.0 !=1.5.0" +matching: + example: + content: [foo, bar, moo] + matches: + and: + - contain-elements: [foo, bar] + - [foo, bar] # same as above + - equal: [foo, bar, moo] # order matters, exact match + - consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers + - contain-element: + have-prefix: b ``` -For more information see: -* [gomega_test.go](https://github.com/goss-org/goss/blob/master/resource/gomega_test.go) - For a complete set of supported json -> Gomega mapping -* [gomega](https://onsi.github.io/gomega/) - Gomega matchers reference -* [semver](https://github.com/blang/semver#ranges) - Semver constraint (or range) syntax +#### Misc matchers + +These matchers don't really fall into any of the above categories, or span multiple categories. + +* `equal` - Useful when needing to override a default matcher +* `have-len: 3` - Checks if the array/string/map has length of 3 +* `have-key: "foo"` - Checks if key exists in map, useful with `gjson` +* `not: matcher` - Checks that a matcher does not match +* `and: [matcher, ..]` - Checks that all matchers match +* `or: [matcher, ..]` - Checks that any matchers match + * when system returns a string it is converted into a one element array and matched + +See the following for examples: [link..]fixme + +##### semver-constraint + +Checks that all versions match semver constraint or range syntax. This uses [semver](https://github.com/blang/semver) under the hood, however, wildcards (e.g. `1.X` are not officially supported and may go away in a future release. + +Example: +```yaml +matching: + semver: + content: + - 1.0.1 + - 1.9.9 + matches: + semver-constraint: ">1.0.0 <2.0.0 !=1.5.0" + semver2: + content: + - 1.0.1 + - 1.5.0 + - 1.9.9 + matches: + not: + semver-constraint: ">1.0.0 <2.0.0 !=1.5.0" + semver3: + content: 1.0.1 + matches: + semver-constraint: ">5.0.0 || < 1.5.0" +``` + + +##### gjson + +Checks extracted [gjson](https://gjson.dev/) passes the matcher + +Example: +```yaml +matching: + example: + content: '{"foo": "bar", "moo" {"nested": "cow"}, "count": "15"}' + matches: + gjson: + moo.nested: cow + foo: {have-prefix: b} + count: {le: 25} + '@this': {have-key: "foo"} + moo: + and: + - {have-key: "nested"} + - {not: {have-key: "nested2"}} +``` ## Templates diff --git a/docs/platform-feature-parity.md b/docs/platform-feature-parity.md index cba5e3a1..c5f38850 100644 --- a/docs/platform-feature-parity.md +++ b/docs/platform-feature-parity.md @@ -4,7 +4,8 @@ macOS and Windows binaries are new and considered alpha-quality. Some functional To clearly signal that, goss emits a log message on every invocation saying so, linking here, then exits with a clear error. -To try out the alpha functionality, you must do one of +To try out the alpha functionality, you must do one of: + * pass `--use-alpha=1` to the root command - e.g. `goss --use-alpha=1 validate`. * set an environment variable `GOSS_USE_ALPHA=1`. @@ -140,8 +141,8 @@ You can find goss-files that are used to populate this matrix within `integratio Run all of the `darwin`/`windows` integration tests: ```bash -make alpha-test-alpha-darwin-amd64 -make alpha-test-alpha-windows-amd64 +make test-int-validate-darwin-amd64 +make test-int-validate-windows-amd64 ``` The script finds all goss spec files within `integration-tests` then filters to just ones matching the passed OS-name, then runs `validate` against them. @@ -153,11 +154,11 @@ This is a special-case test since it requires a persistent process, then to make #### macOS `serve` ```bash -make "test-serve-alpha-darwin-amd64" +make "test-int-serve-darwin-amd64" ``` #### Windows `serve` ```bash -make "test-serve-alpha-windows-amd64" +make "test-int-serve-windows-amd64" ``` diff --git a/examples/goss.yaml b/examples/goss.yaml new file mode 100644 index 00000000..1265c849 --- /dev/null +++ b/examples/goss.yaml @@ -0,0 +1,25 @@ +gossfile: + goss_awesome_gomega.yaml: {} + +file: + test.txt: + exists: true + contains: | + test file + second line + +command: + echo '15': + exit-status: 0 + stdout: + and: + - gt: 10 + - lt: 50 + - match-regexp: '\d{2}' + timeout: 10000 + +http: + https://ifconfig.me: + status: 200 + timeout: 5000 + body: '{{.Vars.Ip}}' diff --git a/examples/goss_awesome_gomega.yaml b/examples/goss_awesome_gomega.yaml new file mode 100644 index 00000000..1a9a54aa --- /dev/null +++ b/examples/goss_awesome_gomega.yaml @@ -0,0 +1,138 @@ +matching: + # Basic matchers + basic_string: + content: 'this is a test' + matches: 'this is a test' + + basic_int: + content: 42 + matches: 42 + + basic_array: + content: + - 'group1' + - 'group2' + - 'group3' + matches: + - 'group1' + - 'group2' + + basic_reader: + as-reader: true + content: | + foo bar + moo cow + matches: + - 'foo' + - '/^m.*w$/' + - '!wtf' + - '!/^ERROR:/' + + # Transformers + basic_reader_as_array: + as-reader: true + content: | + foo bar + moo cow + matches: + and: + - contain-element: {contain-substring: 'foo'} + - contain-element: {match-regexp: '^m.*w$'} + - not: {contain-substring: 'wtf'} + - not: {match-regexp: '^ERROR:'} + + test_numeric_string: + content: 128 + matches: + and: + - '128' + - have-prefix: '1' + - have-suffix: '8' + - match-regexp: '\d{3}' + + test_string_numeric: + content: '128' + matches: + and: + - 128 + - 128.0 + - le: 128 + - gt: 120 + + test_string_float: + content: '128.3' + matches: + and: + - 128.3 + - le: 129 + - gt: 120.2 + + test_array: + content: + - '45' + - '46' + - '47' + matches: + - contain-element: {match-regexp: "4."} + - '45' + - and: [{ge: 46}, {le: 50}] + + test_reader_using_string_matchers: + content: | + foo bar + 15 + moo cow + as-reader: true + matches: + and: + - have-len: 19 + - | + foo bar + 15 + moo cow + - have-prefix: 'foo' + - have-suffix: "cow\n" + - contain-element: + have-prefix: 'moo' + - contain-elements: + - not: 'this_doesnt_exist' + - lt: 20 + - have-prefix: 'moo' + + + test_reader_as_single_string: + content: 'cool' + as-reader: true + matches: 'cool' + + test_reader_using_int_matchers: + content: '40' + as-reader: true + matches: + and: + - le: 250 + - ge: 20 + + + test_gjson_transform: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + moo.nested: cow + foo: {have-prefix: b} + count: {le: 25} + '@this': {have-key: "foo"} + moo: + and: + - {have-key: "nested"} + - {not: {have-key: "nested2"}} + + test_gjson_using_this_and_equal: + content: '{"foo": "bar", "baz": "bing"}' + matches: + gjson: + '@this': + equal: + foo: bar + baz: bing diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 00000000..350a9df1 --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,3 @@ +# How to run this + +Basically, run the following: `goss --vars-inline "Ip: $EXTERNAL_IP" v` diff --git a/examples/test.txt b/examples/test.txt new file mode 100644 index 00000000..ae7cb70b --- /dev/null +++ b/examples/test.txt @@ -0,0 +1,2 @@ +test file +second line diff --git a/extras/dgoss/README.md b/extras/dgoss/README.md index 9bdea5fd..2282b763 100644 --- a/extras/dgoss/README.md +++ b/extras/dgoss/README.md @@ -3,8 +3,8 @@ dgoss is a convenience wrapper around goss that aims to bring the simplicity of goss to docker containers. ## Examples and Tutorials -* [video tutorial](https://youtu.be/PEHz5EnZ-FM) - Introduction to dgoss tutorial -* [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 +* [blog tutorial](https://medium.com/@aelsabbahy/tutorial-how-to-test-your-docker-image-in-half-a-second-bbd13e06a4a9) - Introduction to dgoss tutorial +* [video tutorial](https://youtu.be/PEHz5EnZ-FM) - Same as above, but in video format * [dgoss-examples](https://github.com/aelsabbahy/dgoss-examples) - Repo containing examples of using dgoss to validate docker images ## Installation diff --git a/go.mod b/go.mod index 6d044325..685486c9 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,22 @@ require ( github.com/cheekybits/genny v1.0.0 github.com/fatih/color v1.15.0 github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d - github.com/goss-org/go-ps v0.0.0-20230101144351-953ade48a71b + github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5 github.com/hashicorp/logutils v1.0.0 + github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 github.com/miekg/dns v1.1.52 github.com/moby/sys/mountinfo v0.6.2 github.com/oleiade/reflections v1.0.1 github.com/onsi/gomega v1.27.4 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/prometheus/client_golang v1.14.0 - github.com/prometheus/common v0.42.0 + github.com/prometheus/client_golang v1.7.1 + github.com/prometheus/common v0.10.0 + github.com/samber/lo v1.38.1 github.com/stretchr/testify v1.8.1 + github.com/tidwall/gjson v1.14.4 github.com/urfave/cli v1.22.12 gopkg.in/yaml.v2 v2.4.0 + gotest.tools/v3 v3.0.3 ) require ( @@ -40,13 +44,17 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pkg/errors v0.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sys v0.6.0 // indirect diff --git a/go.sum b/go.sum index 26d18c36..7a4f0016 100644 --- a/go.sum +++ b/go.sum @@ -7,10 +7,17 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 h1:NYoPVh1XuUB5VBWLXRKoqzQhl4bajIxh+XuURbJ0uwc= github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2/go.mod h1:DCNKSpXhum14Y258jSbRmJvcesbzEdBPincz7yJUx3k= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= @@ -20,50 +27,73 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d h1:50mlZKtg8BUvBtFs0ioVpSgMMwcKaJefg/2pZ+lQf98= github.com/goss-org/GOnetstat v0.0.0-20230101144325-22be0bd9e64d/go.mod h1:MBdRlloGIbpQVDuH5Gxg3hjqwZBCZsmFqbYPaeR6r0M= -github.com/goss-org/go-ps v0.0.0-20230101144351-953ade48a71b h1:IVeeIVrdgyMG817dquwmYwcB9TFwphQAhLOiJ5lt8Nc= -github.com/goss-org/go-ps v0.0.0-20230101144351-953ade48a71b/go.mod h1:5oH/hhdJNuFnJTAoXErJ2Jr8LOkbkzosMZT93MrlMOc= +github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5 h1:NW0Jo4leMIrQxNOyOkBu4yBnygI37m0Ey0EUUgvzr+8= +github.com/goss-org/go-ps v0.0.0-20230609005227-7b318e6a56e5/go.mod h1:FYj70SLmogHdTTDGnIVaaK0iczROlsxmoMCwfAUuIE8= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 h1:nHoRIX8iXob3Y2kdt9KsjyIb7iApSvb3vgsd93xb5Ow= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo= github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c= github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -74,95 +104,114 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM= github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60= -github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= -github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= -github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= -github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -170,38 +219,41 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/goss_test.go b/goss_test.go index a43d3f72..16f23084 100644 --- a/goss_test.go +++ b/goss_test.go @@ -5,9 +5,9 @@ import ( "encoding/json" "os" "testing" - "time" "github.com/goss-org/goss/outputs" + "github.com/goss-org/goss/resource" "github.com/goss-org/goss/util" ) @@ -84,13 +84,13 @@ func TestUseAsPackage(t *testing.T) { for _, r := range rg { found++ - if r.Successful { + if r.Result == resource.SUCCESS { passed++ } } } - code, err := Validate(cfg, time.Now()) + code, err := Validate(cfg) checkErr(t, err, "check failed") if code != 0 { t.Fatalf("check failed, expected 0 got %d", code) @@ -110,7 +110,7 @@ func TestUseAsPackage(t *testing.T) { okcount := 0 for _, r := range res.Results { - if r.Successful { + if r.Result == resource.SUCCESS { okcount++ } } @@ -149,7 +149,7 @@ func TestSkipResourcesByType(t *testing.T) { } } - if skipped != 6 { - t.Fatalf("Expected to skip 6 tests, skipped %d", skipped) + if skipped != 5 { + t.Fatalf("Expected to skip 5 tests, skipped %d", skipped) } } diff --git a/install.sh b/install.sh index d1c8d8c2..365a8be3 100644 --- a/install.sh +++ b/install.sh @@ -3,8 +3,9 @@ { set -e -LATEST_URL="https://github.com/goss-org/goss/releases/latest" -LATEST_EFFECTIVE=$(curl -s -L -o /dev/null ${LATEST_URL} -w '%{url_effective}') +# LATEST_URL="https://github.com/goss-org/goss/releases/latest" +# LATEST_EFFECTIVE=$(curl -s -L -o /dev/null ${LATEST_URL} -w '%{url_effective}') +LATEST_EFFECTIVE=https://github.com/goss-org/goss/releases/tag/v0.3.23 LATEST=${LATEST_EFFECTIVE##*/} DGOSS_VER=$GOSS_VER diff --git a/integration-tests/Dockerfile_trusty b/integration-tests/Dockerfile_trusty index 975b39eb..04e0f823 100644 --- a/integration-tests/Dockerfile_trusty +++ b/integration-tests/Dockerfile_trusty @@ -4,8 +4,7 @@ MAINTAINER Ahmed RUN apt-get update && \ apt-get install -y apache2=2.4.7-1ubuntu4.22 tinyproxy && \ apt-get remove -y vim-tiny && \ - apt-get clean && \ - echo manual > /etc/init/apache2.override + apt-get clean RUN sed -i '/reload|force-reload)/i status) pidof tinyproxy > /dev/null && echo "tinyproxy is running";;' /etc/init.d/tinyproxy RUN sed -i '/start)/a\ touch /var/log/tinyproxy/tinyproxy.log /var/run/tinyproxy/tinyproxy.pid' /etc/init.d/tinyproxy diff --git a/integration-tests/goss/alpine3/goss-expected-q.yaml b/integration-tests/goss/alpine3/goss-expected-q.yaml index 9aa197cb..e6f17cf0 100644 --- a/integration-tests/goss/alpine3/goss-expected-q.yaml +++ b/integration-tests/goss/alpine3/goss-expected-q.yaml @@ -1,10 +1,10 @@ file: /etc/passwd: exists: true - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: apache2: installed: true @@ -111,16 +111,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/alpine3/goss-expected.yaml b/integration-tests/goss/alpine3/goss-expected.yaml index 99cef178..27da2b82 100644 --- a/integration-tests/goss/alpine3/goss-expected.yaml +++ b/integration-tests/goss/alpine3/goss-expected.yaml @@ -2,14 +2,13 @@ file: /etc/passwd: exists: true mode: "0644" - size: 1287 owner: root group: root filetype: file - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: apache2: installed: true @@ -154,16 +153,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/centos7/goss-aa-expected.yaml b/integration-tests/goss/centos7/goss-aa-expected.yaml index 13ba204d..a7aa4ce4 100644 --- a/integration-tests/goss/centos7/goss-aa-expected.yaml +++ b/integration-tests/goss/centos7/goss-aa-expected.yaml @@ -2,7 +2,7 @@ package: httpd: installed: true versions: - - 2.4.6 + - 2.4.6-95.el7.centos port: tcp:80: listening: true diff --git a/integration-tests/goss/centos7/goss-expected-q.yaml b/integration-tests/goss/centos7/goss-expected-q.yaml index 345c983e..349eda8f 100644 --- a/integration-tests/goss/centos7/goss-expected-q.yaml +++ b/integration-tests/goss/centos7/goss-expected-q.yaml @@ -1,10 +1,10 @@ file: /etc/passwd: exists: true - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: foobar: installed: false @@ -111,16 +111,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/centos7/goss-expected.yaml b/integration-tests/goss/centos7/goss-expected.yaml index 1d82b5da..8db6d464 100644 --- a/integration-tests/goss/centos7/goss-expected.yaml +++ b/integration-tests/goss/centos7/goss-expected.yaml @@ -2,21 +2,20 @@ file: /etc/passwd: exists: true mode: "0644" - size: 810 owner: root group: root filetype: file - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: foobar: installed: false httpd: installed: true versions: - - 2.4.6 + - 2.4.6-95.el7.centos vim-tiny: installed: false addr: @@ -160,16 +159,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/centos7/goss.yaml b/integration-tests/goss/centos7/goss.yaml index a48058ab..818b2c34 100644 --- a/integration-tests/goss/centos7/goss.yaml +++ b/integration-tests/goss/centos7/goss.yaml @@ -1,4 +1,3 @@ ---- service: autofs: enabled: false @@ -32,4 +31,3 @@ gossfile: "../goss-s*.yaml": {} bypath: file: "../goss-dummy.yaml" - diff --git a/integration-tests/goss/darwin/commands/add.goss.yaml b/integration-tests/goss/darwin/commands/add.goss.yaml index c07a27f9..cd75e38c 100644 --- a/integration-tests/goss/darwin/commands/add.goss.yaml +++ b/integration-tests/goss/darwin/commands/add.goss.yaml @@ -3,7 +3,7 @@ command: "add addr 127.0.0.1": exit-status: 0 - exec: release/goss-alpha-darwin-amd64 --use-alpha=1 add addr 127.0.0.1 + exec: release/goss-darwin-amd64 --use-alpha=1 add addr 127.0.0.1 stdout: - "timeout: 500" stderr: [] diff --git a/integration-tests/goss/darwin/commands/autoadd.goss.yaml b/integration-tests/goss/darwin/commands/autoadd.goss.yaml index f2243cf9..830244ba 100644 --- a/integration-tests/goss/darwin/commands/autoadd.goss.yaml +++ b/integration-tests/goss/darwin/commands/autoadd.goss.yaml @@ -2,7 +2,7 @@ command: "autoadd /Users/travis": exit-status: 0 - exec: "release/goss-alpha-darwin-amd64 --use-alpha=1 autoadd /Users/travis" + exec: "release/goss-darwin-amd64 --use-alpha=1 autoadd /Users/travis" stdout: - 'file:' - ' exists: true' diff --git a/integration-tests/goss/darwin/commands/help.goss.yaml b/integration-tests/goss/darwin/commands/help.goss.yaml index 7af8f274..d57d2bb6 100644 --- a/integration-tests/goss/darwin/commands/help.goss.yaml +++ b/integration-tests/goss/darwin/commands/help.goss.yaml @@ -2,7 +2,7 @@ command: help: exit-status: 0 - exec: "release/goss-alpha-darwin-amd64 help" + exec: "release/goss-darwin-amd64 help" stdout: - alpha stderr: [] diff --git a/integration-tests/goss/darwin/commands/validate.goss.yaml b/integration-tests/goss/darwin/commands/validate.goss.yaml index edf22cad..6d79a6d6 100644 --- a/integration-tests/goss/darwin/commands/validate.goss.yaml +++ b/integration-tests/goss/darwin/commands/validate.goss.yaml @@ -3,7 +3,7 @@ command: "validate": exit-status: 0 - exec: "release/goss-alpha-darwin-amd64 --use-alpha=1 -g integration-tests/goss/darwin/commands/validate-input.yaml validate" + exec: "release/goss-darwin-amd64 --use-alpha=1 -g integration-tests/goss/darwin/commands/validate-input.yaml validate" stdout: - 'Count: 1' - 'Failed: 0' diff --git a/integration-tests/goss/darwin/tests/file.goss.yaml b/integration-tests/goss/darwin/tests/file.goss.yaml index 0f428a23..347494ea 100644 --- a/integration-tests/goss/darwin/tests/file.goss.yaml +++ b/integration-tests/goss/darwin/tests/file.goss.yaml @@ -9,6 +9,6 @@ file: filetype: file md5: 9dcea4037b1439a2a96e4d206eda63a4 sha256: e73d885411a52a0d29142e830e104e0cc9252fbb1dc3c92a430ef7c369f089ef - contains: + contents: - "nothing to see here" - "/nothing.*here/" diff --git a/integration-tests/goss/generate_goss.sh b/integration-tests/goss/generate_goss.sh index 0cdfc3da..93e822fb 100755 --- a/integration-tests/goss/generate_goss.sh +++ b/integration-tests/goss/generate_goss.sh @@ -59,10 +59,14 @@ goss a "${args[@]}" process $package foobar goss a "${args[@]}" kernel-param kernel.ostype goss a "${args[@]}" mount /dev +# Make tests consistent across different docker setups +sed -i '/- seclabel/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml +sed -i '/- size=/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml +sed -i '/- mode=/d' $SCRIPT_DIR/${OS}/goss-generated-$ARCH.yaml goss a "${args[@]}" http https://www.google.com -goss a "${args[@]}" http https://www.microsoft.com -x http://127.0.0.1:8888 +goss a "${args[@]}" http https://www.apple.com -x http://127.0.0.1:8888 goss a "${args[@]}" http http://google.com -r @@ -78,6 +82,7 @@ $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.ya $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa $package # Validate that we can aa none existent resources without destroying the file $SCRIPT_DIR/$OS/goss-linux-$ARCH -g $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml aa nosuchresource + if [[ ! -f $SCRIPT_DIR/${OS}/goss-aa-generated-$ARCH.yaml ]] then echo "Error! Config file removed by aa!" && exit 1 diff --git a/integration-tests/goss/goss-shared.yaml b/integration-tests/goss/goss-shared.yaml index 9b794bb2..ce5a1f5e 100644 --- a/integration-tests/goss/goss-shared.yaml +++ b/integration-tests/goss/goss-shared.yaml @@ -25,7 +25,7 @@ file: owner: root group: root filetype: file - contains: + contents: - root {{end}} "/goss/hellogoss.txt": @@ -35,7 +35,7 @@ file: sha512: 372864ab83187de41ca57c5c77cd4a99220ccadc8b8ddb18367893fd3e58764193a599edbf63a48c0c44f1e923606a00929b46de3bda1744fd722b9d42829206 "/tmp/goss/foobar": exists: false - contains: [] + contents: [] "~root": exists: true mode: '0700' @@ -50,7 +50,7 @@ file: filetype: pipe "/does/not/exist": exists: true - contains: + contents: - skip-this-test skip: true package: @@ -62,6 +62,13 @@ package: versions: - {{$ver}} {{end}} +service: +{{- range $name, $runlevels := index .Vars .Env.OS "services"}} + {{$name}}: + enabled: true + running: true + runlevels: {{toJson $runlevels}} +{{end}} addr: tcp://google.com:22: reachable: false @@ -165,6 +172,7 @@ mount: opts: - rw - nosuid + - mode=755 source: tmpfs filesystem: tmpfs "/": @@ -183,12 +191,13 @@ http: {{ if index .Vars .Env.OS "proxy" }} https://httpbin.org/anything: status: 200 + timeout: 60000 proxy: {{ index .Vars .Env.OS "proxy" }} {{ end }} https://httpbin.org/headers: status: 200 allow-insecure: false - timeout: 5000 + timeout: 60000 request-headers: - "Foo: bar" headers: ["Content-Type: application/json"] @@ -196,7 +205,7 @@ http: https://httpbin.org/headers?host: status: 200 allow-insecure: false - timeout: 5000 + timeout: 60000 request-headers: # This is causing intermittent errors depending on the httpbin server hit # need to see if there's a good way around this, maybe local httpbin? @@ -207,26 +216,31 @@ http: body: ['"Host": "httpbin.org"'] https://httpbin.org/basic-auth/username/secret: status: 200 + timeout: 60000 username: username password: secret https://httpbin.org/basic-auth/username/secret?failure: status: 401 + timeout: 60000 username: username password: wrong https://httpbin.org/put: status: 200 method: PUT + timeout: 60000 request-body: '{"key": "value"}' body: - '"key": "value"' anything-with-get: url: https://httpbin.org/anything status: 200 + timeout: 60000 body: [] anything-with-put: url: https://httpbin.org/anything status: 200 method: GET + timeout: 60000 request-body: "request-body" body: ["request-body"] matching: @@ -245,8 +259,6 @@ matching: baz: bing matches: and: - - have-key-with-value: - foo: bar - have-key: baz semver: content: diff --git a/integration-tests/goss/trusty/goss-aa-expected.yaml b/integration-tests/goss/trusty/goss-aa-expected.yaml index a9a98ed2..8be205ce 100644 --- a/integration-tests/goss/trusty/goss-aa-expected.yaml +++ b/integration-tests/goss/trusty/goss-aa-expected.yaml @@ -10,7 +10,7 @@ port: - 0.0.0.0 service: apache2: - enabled: false + enabled: true running: true process: apache2: diff --git a/integration-tests/goss/trusty/goss-expected-q.yaml b/integration-tests/goss/trusty/goss-expected-q.yaml index c1e6e053..9554f8e8 100644 --- a/integration-tests/goss/trusty/goss-expected-q.yaml +++ b/integration-tests/goss/trusty/goss-expected-q.yaml @@ -1,10 +1,10 @@ file: /etc/passwd: exists: true - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: apache2: installed: true @@ -31,7 +31,7 @@ port: listening: false service: apache2: - enabled: false + enabled: true running: true foobar: enabled: false @@ -111,16 +111,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/trusty/goss-expected.yaml b/integration-tests/goss/trusty/goss-expected.yaml index 9aad00c7..94e59036 100644 --- a/integration-tests/goss/trusty/goss-expected.yaml +++ b/integration-tests/goss/trusty/goss-expected.yaml @@ -2,14 +2,13 @@ file: /etc/passwd: exists: true mode: "0644" - size: 1006 owner: root group: root filetype: file - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: apache2: installed: true @@ -42,7 +41,7 @@ port: ip: [] service: apache2: - enabled: false + enabled: true running: true foobar: enabled: false @@ -160,16 +159,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/vars.yaml b/integration-tests/goss/vars.yaml index ec08626b..cbf6df77 100644 --- a/integration-tests/goss/vars.yaml +++ b/integration-tests/goss/vars.yaml @@ -3,19 +3,27 @@ alpine3: proxy: http://127.0.0.1:8888 packages: apache2: "2.4.46-r1" + services: + apache2: [sysinit] arch: packages: centos7: proxy: http://127.0.0.1:8888 packages: - httpd: "2.4.6" + httpd: "2.4.6-95.el7.centos" + services: + httpd: [] trusty: proxy: http://127.0.0.1:8888 packages: apache2: "2.4.7-1ubuntu4.22" + services: + apache2: ["3"] wheezy: proxy: http://127.0.0.1:8888 packages: apache2: "2.2.22-13+deb7u13" + services: + apache2: ["2", "3", "5", "4"] overwrite: foo diff --git a/integration-tests/goss/wheezy/goss-expected-q.yaml b/integration-tests/goss/wheezy/goss-expected-q.yaml index e37715a7..9554f8e8 100644 --- a/integration-tests/goss/wheezy/goss-expected-q.yaml +++ b/integration-tests/goss/wheezy/goss-expected-q.yaml @@ -1,10 +1,10 @@ file: /etc/passwd: exists: true - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: apache2: installed: true @@ -111,16 +111,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/wheezy/goss-expected.yaml b/integration-tests/goss/wheezy/goss-expected.yaml index d500dff1..27295fbe 100644 --- a/integration-tests/goss/wheezy/goss-expected.yaml +++ b/integration-tests/goss/wheezy/goss-expected.yaml @@ -2,14 +2,13 @@ file: /etc/passwd: exists: true mode: "0644" - size: 812 owner: root group: root filetype: file - contains: [] + contents: [] /tmp/goss/foobar: exists: false - contains: [] + contents: [] package: apache2: installed: true @@ -160,16 +159,16 @@ http: no-follow-redirects: true timeout: 5000 body: [] - https://www.google.com: + https://www.apple.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - https://www.microsoft.com: + proxy: http://127.0.0.1:8888 + https://www.google.com: status: 200 allow-insecure: false no-follow-redirects: false timeout: 5000 body: [] - proxy: http://127.0.0.1:8888 diff --git a/integration-tests/goss/windows/commands/add.goss.yaml b/integration-tests/goss/windows/commands/add.goss.yaml index 8b478489..d65d1899 100644 --- a/integration-tests/goss/windows/commands/add.goss.yaml +++ b/integration-tests/goss/windows/commands/add.goss.yaml @@ -3,7 +3,7 @@ command: "add addr 127.0.0.1": exit-status: 0 - exec: release\goss-alpha-windows-amd64 --use-alpha=1 add addr 127.0.0.1 + exec: release\goss-windows-amd64 --use-alpha=1 add addr 127.0.0.1 stdout: - "timeout: 500" stderr: [] diff --git a/integration-tests/goss/windows/commands/autoadd.goss.yaml b/integration-tests/goss/windows/commands/autoadd.goss.yaml index cd88a70d..5ed311a2 100644 --- a/integration-tests/goss/windows/commands/autoadd.goss.yaml +++ b/integration-tests/goss/windows/commands/autoadd.goss.yaml @@ -2,7 +2,7 @@ command: "autoadd Administrator": exit-status: 0 - exec: release\goss-alpha-windows-amd64 --use-alpha=1 autoadd Administrator + exec: release\goss-windows-amd64 --use-alpha=1 autoadd Administrator stdout: - 'user:' - ' name: Administrator' diff --git a/integration-tests/goss/windows/commands/help.goss.yaml b/integration-tests/goss/windows/commands/help.goss.yaml index d1c5463b..2f8bce52 100644 --- a/integration-tests/goss/windows/commands/help.goss.yaml +++ b/integration-tests/goss/windows/commands/help.goss.yaml @@ -2,7 +2,7 @@ command: help: exit-status: 0 - exec: release\goss-alpha-windows-amd64 help + exec: release\goss-windows-amd64 help stdout: - alpha stderr: [] diff --git a/integration-tests/goss/windows/commands/validate.goss.yaml b/integration-tests/goss/windows/commands/validate.goss.yaml index c504b5ec..fe2456ac 100644 --- a/integration-tests/goss/windows/commands/validate.goss.yaml +++ b/integration-tests/goss/windows/commands/validate.goss.yaml @@ -3,7 +3,7 @@ command: "validate": exit-status: 0 - exec: "release\\goss-alpha-windows-amd64 --use-alpha=1 -g integration-tests/goss/windows/commands/validate-input.yaml validate" + exec: "release\\goss-windows-amd64 --use-alpha=1 -g integration-tests/goss/windows/commands/validate-input.yaml validate" stdout: - 'Count: 1' - 'Failed: 0' diff --git a/integration-tests/goss/windows/tests/command.goss.yaml b/integration-tests/goss/windows/tests/command.goss.yaml index 26a48dcf..e5955ed2 100644 --- a/integration-tests/goss/windows/tests/command.goss.yaml +++ b/integration-tests/goss/windows/tests/command.goss.yaml @@ -11,14 +11,14 @@ command: exec: powershell -noprofile -noninteractive -command (get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous exit-status: 0 stdout: - - 0 + - "0" stderr: [] timeout: 10000 wrap a powershell with quotes - expect 0 because travis does not restrict anonymous logins: exec: powershell -noprofile -noninteractive -command "(get-itemproperty -path 'HKLM:/SYSTEM/CurrentControlSet/Control/Lsa/').restrictanonymous" exit-status: 0 stdout: - - 0 + - "0" stderr: [] timeout: 10000 powershell with quotes: diff --git a/integration-tests/goss/windows/tests/file.goss.yaml b/integration-tests/goss/windows/tests/file.goss.yaml index 80f1b750..a1c3ed0b 100644 --- a/integration-tests/goss/windows/tests/file.goss.yaml +++ b/integration-tests/goss/windows/tests/file.goss.yaml @@ -9,6 +9,6 @@ file: filetype: file md5: dc9a07ca9789f866d21d544fe5651954 sha256: aa8b1b4a0d9bf174f5019c8f8a9568858ee2bdf8e0ad16aec54417d49b48df49 - contains: + contents: - "nothing to see here" - "/nothing.*here/" diff --git a/integration-tests/run-serve-tests.sh b/integration-tests/run-serve-tests.sh index e9c07291..33f8865c 100755 --- a/integration-tests/run-serve-tests.sh +++ b/integration-tests/run-serve-tests.sh @@ -9,10 +9,6 @@ IFS='- ' read -r -a segments <<< "${platform_spec}" os="${segments[0]}" arch="${segments[1]}" -if [[ "${segments[0]}" == "alpha" ]]; then - os="${segments[1]}" - arch="${segments[2]}" -fi find_open_port() { local startAt="${1:?"Supply start of port range"}" diff --git a/integration-tests/run-validate-tests.sh b/integration-tests/run-validate-tests.sh new file mode 100755 index 00000000..90f85c20 --- /dev/null +++ b/integration-tests/run-validate-tests.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# shellcheck source=../ci/lib/setup.sh +source "$(dirname "${BASH_SOURCE[0]}")/../ci/lib/setup.sh" || exit 67 + +platform_spec="${1:?"Must supply name of release binary to build e.g. goss-linux-amd64"}" +# Split platform_spec into platform/arch segments +IFS='- ' read -r -a segments <<< "${platform_spec}" + +os="${segments[0]}" +arch="${segments[1]}" + +if [[ "${os}" == "linux" ]]; then + echo "OS is ${os}. This script is not for running tests on the different flavours of linux." + echo "Linux is exercised via the integration-tests/test.sh currently, because linux can be" + echo "verified via docker containers; macOS and Windows cannot." + echo "This script is for macOS and Windows, and runs tests that are expected to pass on" + echo "Travis-CI provided images, running nakedly (no containerisation) on the hosts there." + exit 1 +fi + +repo_root="$(git rev-parse --show-toplevel)" +export GOSS_BINARY="${repo_root}/release/goss-${platform_spec}" +log_info "Using: '${GOSS_BINARY}', cwd: '$(pwd)', os: ${os}" +readarray -t goss_test_files < <(find integration-tests -type f -name "*.goss.yaml" | grep "${os}" | sort | uniq) + +export GOSS_USE_ALPHA=1 +for file in "${goss_test_files[@]}"; do + args=( + "-g=${file}" + "validate" + ) + log_action -e "\nTesting \`${GOSS_BINARY} ${args[*]}\` ...\n" + "${GOSS_BINARY}" "${args[@]}" +done diff --git a/integration-tests/test.sh b/integration-tests/test.sh index 7a354e2a..ed941cac 100755 --- a/integration-tests/test.sh +++ b/integration-tests/test.sh @@ -9,19 +9,6 @@ arch="${2:?"Need arch as 2nd arg. e.g. amd64 386"}" vars_inline="{inline: bar, overwrite: bar}" -seccomp_opts() { - local docker_ver minor_ver - docker_ver=$(docker version -f '{{.Client.Version}}') - minor_ver=$(cut -d'.' -f2 <<<$docker_ver) - major_ver=$(cut -d'.' -f1 <<<$docker_ver) - if ((minor_ver>=10))||((major_ver>18)); then - echo ' --security-opt seccomp:unconfined ' - fi - if ((major_ver>18)); then - echo ' --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro ' - fi -} - # setup places us inside repo-root; this preserves current behaviour with least change. cd integration-tests @@ -43,7 +30,7 @@ docker_exec() { if docker ps -a | grep "$container_name";then docker rm -vf "$container_name" fi -opts=(--env OS=$os --cap-add SYS_ADMIN -v "$PWD/goss:/goss" -d --name "$container_name" $(seccomp_opts)) +opts=(--env OS=$os --cap-add SYS_ADMIN -v "$PWD/goss:/goss" -d --name "$container_name" --security-opt seccomp:unconfined --security-opt label:disable) id=$(docker run "${opts[@]}" "aelsabbahy/goss_$os" /sbin/init) ip=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$id") trap "rv=\$?; docker rm -vf $id; exit \$rv" INT TERM EXIT @@ -57,7 +44,7 @@ echo "$out" if [[ $os == "arch" ]]; then egrep -q 'Count: 99, Failed: 0, Skipped: 3' <<<"$out" else - egrep -q 'Count: 119, Failed: 0, Skipped: 5' <<<"$out" + egrep -q 'Count: 120, Failed: 0, Skipped: 5' <<<"$out" fi if [[ ! $os == "arch" ]]; then diff --git a/matcher_test.go b/matcher_test.go new file mode 100644 index 00000000..64703307 --- /dev/null +++ b/matcher_test.go @@ -0,0 +1,86 @@ +//go:build linux + +package goss + +import ( + "bytes" + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/goss-org/goss/util" + "github.com/stretchr/testify/assert" +) + +var ( + // This will generate the "golden files" prior to running the tests. + // helpful when the output is changed and a user doesn't want to update every single expectation file by hand + update = flag.Bool("update", false, "update the golden files of this test") +) + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +func TestMatchers(t *testing.T) { + files, err := filepath.Glob(filepath.Join("testdata", "out_matching_*")) + if err != nil { + t.Fatal(err) + } + + for _, outFile := range files { + outFile := outFile + parts := strings.Split(outFile, ".") + specName := fmt.Sprintf("%s.yaml", strings.TrimPrefix(parts[0], "testdata/out_")) + specFile := filepath.Join("testdata", specName) + outFormat := parts[2] + wantCode, err := strconv.Atoi(parts[1]) + if err != nil { + t.Fatal(err) + } + tn := outFile + t.Run(tn, func(t *testing.T) { + output := &bytes.Buffer{} + + cfg, err := util.NewConfig( + util.WithOutputFormat(outFormat), + util.WithResultWriter(output), + util.WithSpecFile(specFile), + util.WithFormatOptions("sort", "pretty"), + ) + if err != nil { + t.Fatal(err) + } + exitCode, err := Validate(cfg) + actualOut := output.String() + actualOut = sanitizeOutput(actualOut) + + if *update { + os.WriteFile(outFile, []byte(actualOut), 0644) + } + wantOutB, err := os.ReadFile(outFile) + if err != nil { + t.Fatal(err) + } + wantOut := string(wantOutB) + if actualOut != wantOut { + assert.Equal(t, wantOut, actualOut) + } + if exitCode != wantCode { + assert.Equal(t, wantCode, exitCode) + } + }) + } +} + +func sanitizeOutput(s string) string { + // Remove duration time + re := regexp.MustCompile(`\d\.\d\d\ds`) + return re.ReplaceAllString(s, "") +} diff --git a/matchers/and.go b/matchers/and.go new file mode 100644 index 00000000..502083bd --- /dev/null +++ b/matchers/and.go @@ -0,0 +1,50 @@ +package matchers + +import ( + "encoding/json" +) + +type AndMatcher struct { + fakeOmegaMatcher + Matchers []GossMatcher + + // state + firstFailedMatcher GossMatcher +} + +func And(ms ...GossMatcher) GossMatcher { + return &AndMatcher{Matchers: ms} +} + +func (m *AndMatcher) Match(actual interface{}) (success bool, err error) { + m.firstFailedMatcher = nil + for _, matcher := range m.Matchers { + success, err := matcher.Match(actual) + if !success || err != nil { + m.firstFailedMatcher = matcher + return false, err + } + } + return true, nil +} + +func (m *AndMatcher) FailureResult(actual interface{}) MatcherResult { + return m.firstFailedMatcher.FailureResult(actual) +} + +func (m *AndMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to satisfy all of these matchers", + Expected: m.Matchers, + } +} + +func (m *AndMatcher) MarshalJSON() ([]byte, error) { + if len(m.Matchers) == 1 { + return json.Marshal(m.Matchers[0]) + } + j := make(map[string]interface{}) + j["and"] = m.Matchers + return json.Marshal(j) +} diff --git a/matchers/be_numerically_matcher.go b/matchers/be_numerically_matcher.go new file mode 100644 index 00000000..ca23030a --- /dev/null +++ b/matchers/be_numerically_matcher.go @@ -0,0 +1,68 @@ +package matchers + +import ( + "encoding/json" + "fmt" + + "github.com/onsi/gomega/matchers" +) + +type BeNumericallyMatcher struct { + fakeOmegaMatcher + Comparator string + CompareTo []interface{} +} + +func BeNumerically(comparator string, compareTo ...interface{}) GossMatcher { + return &BeNumericallyMatcher{ + Comparator: comparator, + CompareTo: compareTo, + } +} +func (m *BeNumericallyMatcher) Match(actual interface{}) (success bool, err error) { + comparator, err := strToSymbol(m.Comparator) + if err != nil { + return false, err + } + matcher := &matchers.BeNumericallyMatcher{ + Comparator: comparator, + CompareTo: m.CompareTo, + } + return matcher.Match(actual) +} + +func (m *BeNumericallyMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: fmt.Sprintf("to be numerically %s", m.Comparator), + Expected: m.CompareTo[0], + } +} + +func (m *BeNumericallyMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: fmt.Sprintf("not to be numerically %s", m.Comparator), + Expected: m.CompareTo[0], + } +} + +func (m *BeNumericallyMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j[m.Comparator] = m.CompareTo[0] + return json.Marshal(j) +} + +func strToSymbol(s string) (string, error) { + comparator, ok := map[string]string{ + "gt": ">", + "ge": ">=", + "lt": "<", + "le": "<=", + "eq": "==", + }[s] + if !ok { + return "", fmt.Errorf("Unknown comparator: %s", s) + } + return comparator, nil +} diff --git a/matchers/consist_of.go b/matchers/consist_of.go new file mode 100644 index 00000000..94a11401 --- /dev/null +++ b/matchers/consist_of.go @@ -0,0 +1,45 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type ConsistOfMatcher struct { + matchers.ConsistOfMatcher +} + +func ConsistOf(elements ...interface{}) GossMatcher { + return &ConsistOfMatcher{ + matchers.ConsistOfMatcher{ + Elements: elements, + }, + } +} + +func (m *ConsistOfMatcher) FailureResult(actual interface{}) MatcherResult { + missingElements := getUnexported(m, "missingElements") + extraElements := getUnexported(m, "extraElements") + return MatcherResult{ + Actual: actual, + Message: "to consist of", + Expected: m.Elements, + MissingElements: missingElements, + ExtraElements: extraElements, + } +} + +func (m *ConsistOfMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to consist of", + Expected: m.Elements, + } +} + +func (m *ConsistOfMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["consist-of"] = m.Elements + return json.Marshal(j) +} diff --git a/matchers/contain_element_matcher.go b/matchers/contain_element_matcher.go new file mode 100644 index 00000000..30a125e1 --- /dev/null +++ b/matchers/contain_element_matcher.go @@ -0,0 +1,41 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type ContainElementMatcher struct { + matchers.ContainElementMatcher +} + +func ContainElement(element interface{}) GossMatcher { + return &ContainElementMatcher{ + matchers.ContainElementMatcher{ + Element: element, + }, + } +} + +func (m *ContainElementMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to contain element matching", + Expected: m.Element, + } +} + +func (m *ContainElementMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to contain element matching", + Expected: m.Element, + } +} + +func (m *ContainElementMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["contain-element"] = m.Element + return json.Marshal(j) +} diff --git a/matchers/contain_elements_matcher.go b/matchers/contain_elements_matcher.go new file mode 100644 index 00000000..1d478594 --- /dev/null +++ b/matchers/contain_elements_matcher.go @@ -0,0 +1,43 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type ContainElementsMatcher struct { + matchers.ContainElementsMatcher +} + +func ContainElements(elements ...interface{}) GossMatcher { + return &ContainElementsMatcher{ + matchers.ContainElementsMatcher{ + Elements: elements, + }, + } +} +func (m *ContainElementsMatcher) FailureResult(actual interface{}) MatcherResult { + missingElements := getUnexported(m, "missingElements") + return MatcherResult{ + Actual: actual, + Message: "to contain elements", + Expected: m.Elements, + MissingElements: missingElements, + } + +} +func (m *ContainElementsMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to contain elements", + Expected: m.Elements, + } + +} + +func (m *ContainElementsMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["contain-elements"] = m.Elements + return json.Marshal(j) +} diff --git a/matchers/contain_substring_matcher.go b/matchers/contain_substring_matcher.go new file mode 100644 index 00000000..851218d8 --- /dev/null +++ b/matchers/contain_substring_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type ContainSubstringMatcher struct { + matchers.ContainSubstringMatcher +} + +func ContainSubstring(substr string, args ...interface{}) GossMatcher { + return &ContainSubstringMatcher{ + matchers.ContainSubstringMatcher{ + Substr: substr, + Args: args, + }, + } +} + +func (m *ContainSubstringMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to contain substring", + Expected: m.Substr, + } +} + +func (m *ContainSubstringMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to contain substring", + Expected: m.Substr, + } +} + +func (m *ContainSubstringMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["contain-substring"] = m.Substr + return json.Marshal(j) +} diff --git a/matchers/equal_matcher.go b/matchers/equal_matcher.go new file mode 100644 index 00000000..faa2125e --- /dev/null +++ b/matchers/equal_matcher.go @@ -0,0 +1,39 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type EqualMatcher struct { + matchers.EqualMatcher +} + +func Equal(element interface{}) GossMatcher { + return &EqualMatcher{ + matchers.EqualMatcher{ + Expected: element, + }, + } +} + +func (m *EqualMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to equal", + Expected: m.Expected, + } +} + +func (m *EqualMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to equal", + Expected: m.Expected, + } +} + +func (m *EqualMatcher) MarshalJSON() ([]byte, error) { + return json.Marshal(m.Expected) +} diff --git a/matchers/have_key_matcher.go b/matchers/have_key_matcher.go new file mode 100644 index 00000000..624203c2 --- /dev/null +++ b/matchers/have_key_matcher.go @@ -0,0 +1,41 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type HaveKeyMatcher struct { + matchers.HaveKeyMatcher +} + +func HaveKey(key interface{}) GossMatcher { + return &HaveKeyMatcher{ + matchers.HaveKeyMatcher{ + Key: key, + }, + } +} + +func (m *HaveKeyMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to have key matching", + Expected: m.Key, + } +} + +func (m *HaveKeyMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to have key matching", + Expected: m.Key, + } +} + +func (m *HaveKeyMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["have-key"] = m.Key + return json.Marshal(j) +} diff --git a/matchers/have_len_matcher.go b/matchers/have_len_matcher.go new file mode 100644 index 00000000..f6b1038e --- /dev/null +++ b/matchers/have_len_matcher.go @@ -0,0 +1,41 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type HaveLenMatcher struct { + matchers.HaveLenMatcher +} + +func HaveLen(count int) GossMatcher { + return &HaveLenMatcher{ + matchers.HaveLenMatcher{ + Count: count, + }, + } +} + +func (m *HaveLenMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to have length", + Expected: m.Count, + } +} + +func (m *HaveLenMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to have length", + Expected: m.Count, + } +} + +func (m *HaveLenMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["have-len"] = m.Count + return json.Marshal(j) +} diff --git a/matchers/have_patterns.go b/matchers/have_patterns.go new file mode 100644 index 00000000..cff7032f --- /dev/null +++ b/matchers/have_patterns.go @@ -0,0 +1,276 @@ +package matchers + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "regexp" + "strings" + + "github.com/onsi/gomega/format" +) + +const ( + maxScanTokenSize = 1024 * 1024 +) + +type HavePatternsMatcher struct { + fakeOmegaMatcher + + Elements interface{} + missingElements []string +} + +func HavePatterns(elements interface{}) GossMatcher { + return &HavePatternsMatcher{ + Elements: elements, + } +} + +func (m *HavePatternsMatcher) Match(actual interface{}) (success bool, err error) { + t, ok := m.Elements.([]interface{}) + if !ok { + return false, fmt.Errorf("HavePatterns matcher expects an array of matchers. Got:\n%s", format.Object(m.Elements, 1)) + } + elements := make([]string, len(t)) + for i, v := range t { + switch v := v.(type) { + case string: + elements[i] = v + default: + return false, fmt.Errorf("HavePatterns matcher expects patterns to be a string. got: \n%s", format.Object(v, 1)) + } + } + notfound, err := sliceToPatterns(elements) + if err != nil { + return false, err + } + // short circuit + if len(notfound) == 0 { + return true, nil + } + var fh io.Reader + switch av := actual.(type) { + case io.Reader: + fh = av + case string: + fh = strings.NewReader(av) + case []string: + fh = strings.NewReader(strings.Join(av, "\n")) + default: + err = fmt.Errorf("Incorrect type %T", actual) + + } + if err != nil { + return false, err + } + + defer func() { + if rc, ok := fh.(io.ReadCloser); ok { + rc.Close() + } + }() + + scanner := bufio.NewScanner(fh) + scanner.Buffer(nil, maxScanTokenSize) + var found []patternMatcher + for scanner.Scan() { + line := scanner.Text() + + i := 0 + for _, pat := range notfound { + if pat.Match(line) { + // Found it, but wasn't supposed to, don't mark it as found, but remove it from search + if !pat.Inverse() { + found = append(found, pat) + } + continue + } + notfound[i] = pat + i++ + } + notfound = notfound[:i] + if len(notfound) == 0 { + break + } + } + if err := scanner.Err(); err != nil { + return false, err + } + + for _, pat := range notfound { + // Didn't find it, but we didn't want to.. so we mark it as found + // Empty pattern should match even if input to scanner is empty + if pat.Inverse() || pat.Pattern() == "" { + found = append(found, pat) + } + } + + if len(elements) != len(found) { + found := patternsToSlice(found) + m.missingElements = subtractSlice(elements, found) + return false, nil + } + return true, nil +} + +func (m *HavePatternsMatcher) FailureResult(actual interface{}) MatcherResult { + var a interface{} + switch actual.(type) { + case string, []string: + a = actual + default: + a = fmt.Sprintf("object: %T", actual) + } + return MatcherResult{ + Actual: a, + Message: "to have patterns", + Expected: m.Elements, + MissingElements: m.missingElements, + } +} + +func (m *HavePatternsMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + a, ok := actual.(string) + if !ok { + a = fmt.Sprintf("object: %T", actual) + } + return MatcherResult{ + Actual: a, + Message: "not to have patterns", + Expected: m.Elements, + } +} + +func appendMissingStrings(message string, missingElements []string) string { + if len(missingElements) == 0 { + return message + } + return fmt.Sprintf("%s\nthe missing elements were\n%s", message, + format.Object(missingElements, 1)) +} + +type patternMatcher interface { + Match(string) bool + Pattern() string + Inverse() bool +} + +type stringPattern struct { + pattern string + cleanPattern string + inverse bool +} + +func newStringPattern(str string) *stringPattern { + var inverse bool + if strings.HasPrefix(str, "!") { + inverse = true + } + cleanPattern := strings.TrimLeft(str, "\\/!") + return &stringPattern{ + pattern: str, + cleanPattern: cleanPattern, + inverse: inverse, + } +} + +func (s *stringPattern) Match(str string) bool { + return strings.Contains(str, s.cleanPattern) +} + +func (s *stringPattern) Pattern() string { return s.pattern } +func (s *stringPattern) Inverse() bool { return s.inverse } + +type regexPattern struct { + pattern string + re *regexp.Regexp + inverse bool +} + +func newRegexPattern(str string) (*regexPattern, error) { + var inverse bool + cleanStr := str + if strings.HasPrefix(str, "!") { + inverse = true + cleanStr = cleanStr[1:] + } + trimLeft := []rune{'\\', '/'} + for _, r := range trimLeft { + if rune(cleanStr[0]) == r { + cleanStr = cleanStr[1:] + break + } + } + trimRight := []rune{'/'} + for _, r := range trimRight { + if rune(cleanStr[len(cleanStr)-1]) == r { + cleanStr = cleanStr[:len(cleanStr)-1] + break + } + } + + re, err := regexp.Compile(cleanStr) + + return ®exPattern{ + pattern: str, + re: re, + inverse: inverse, + }, err + +} + +func (re *regexPattern) Match(str string) bool { + return re.re.MatchString(str) +} + +func (re *regexPattern) Pattern() string { return re.pattern } +func (re *regexPattern) Inverse() bool { return re.inverse } + +func sliceToPatterns(slice []string) ([]patternMatcher, error) { + var patterns []patternMatcher + for _, s := range slice { + if (strings.HasPrefix(s, "/") || strings.HasPrefix(s, "!/")) && strings.HasSuffix(s, "/") { + pat, err := newRegexPattern(s) + if err != nil { + return nil, err + } + patterns = append(patterns, pat) + } else { + patterns = append(patterns, newStringPattern(s)) + } + } + return patterns, nil +} + +func patternsToSlice(patterns []patternMatcher) []string { + var slice []string + for _, p := range patterns { + slice = append(slice, p.Pattern()) + } + return slice +} +func subtractSlice(x, y []string) []string { + m := make(map[string]bool) + + for _, y := range y { + m[y] = true + } + + var ret []string + for _, x := range x { + if m[x] { + continue + } + ret = append(ret, x) + } + + return ret +} + +func (matcher *HavePatternsMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["have-patterns"] = matcher.Elements + return json.Marshal(j) +} diff --git a/matchers/have_prefix_matcher.go b/matchers/have_prefix_matcher.go new file mode 100644 index 00000000..a9c32eb8 --- /dev/null +++ b/matchers/have_prefix_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type HavePrefixMatcher struct { + matchers.HavePrefixMatcher +} + +func HavePrefix(prefix string, args ...interface{}) GossMatcher { + return &HavePrefixMatcher{ + matchers.HavePrefixMatcher{ + Prefix: prefix, + Args: args, + }, + } +} + +func (m *HavePrefixMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to have prefix", + Expected: m.Prefix, + } +} + +func (m *HavePrefixMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to have prefix", + Expected: m.Prefix, + } +} + +func (m *HavePrefixMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["have-prefix"] = m.Prefix + return json.Marshal(j) +} diff --git a/matchers/have_suffix_matcher.go b/matchers/have_suffix_matcher.go new file mode 100644 index 00000000..a4d75929 --- /dev/null +++ b/matchers/have_suffix_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type HaveSuffixMatcher struct { + matchers.HaveSuffixMatcher +} + +func HaveSuffix(prefix string, args ...interface{}) GossMatcher { + return &HaveSuffixMatcher{ + matchers.HaveSuffixMatcher{ + Suffix: prefix, + Args: args, + }, + } +} + +func (m *HaveSuffixMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to have suffix", + Expected: m.Suffix, + } +} + +func (m *HaveSuffixMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to have suffix", + Expected: m.Suffix, + } +} + +func (m *HaveSuffixMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["have-suffix"] = m.Suffix + return json.Marshal(j) +} diff --git a/matchers/match_regexp_matcher.go b/matchers/match_regexp_matcher.go new file mode 100644 index 00000000..aba5c463 --- /dev/null +++ b/matchers/match_regexp_matcher.go @@ -0,0 +1,42 @@ +package matchers + +import ( + "encoding/json" + + "github.com/onsi/gomega/matchers" +) + +type MatchRegexpMatcher struct { + matchers.MatchRegexpMatcher +} + +func MatchRegexp(regexp string, args ...interface{}) GossMatcher { + return &MatchRegexpMatcher{ + matchers.MatchRegexpMatcher{ + Regexp: regexp, + Args: args, + }, + } +} + +func (m *MatchRegexpMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to match regular expression", + Expected: m.Regexp, + } +} + +func (m *MatchRegexpMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to match regular expression", + Expected: m.Regexp, + } +} + +func (m *MatchRegexpMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["match-regexp"] = m.Regexp + return json.Marshal(j) +} diff --git a/matchers/matchers.go b/matchers/matchers.go new file mode 100644 index 00000000..93990f56 --- /dev/null +++ b/matchers/matchers.go @@ -0,0 +1,48 @@ +package matchers + +import ( + "encoding/json" + "reflect" + "unsafe" + + "github.com/onsi/gomega/types" +) + +type GossMatcher interface { + // This is needed due to oMegaMatcher test in some of the GomegaMatcher logic + types.GomegaMatcher + //Match(actual interface{}) (success bool, err error) + FailureResult(actual interface{}) MatcherResult + NegatedFailureResult(actual interface{}) MatcherResult + // This doesn't seem to make a difference, maybe not needed + json.Marshaler +} + +type MatcherResult struct { + Actual interface{} + Message string + Expected interface{} + MissingElements interface{} + ExtraElements interface{} + TransformerChain []Transformer + UntransformedValue interface{} +} + +func getUnexported(i interface{}, field string) interface{} { + rs := reflect.ValueOf(i).Elem() + rf := rs.FieldByName(field) + rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem() + return rf.Interface() +} + +type fakeOmegaMatcher struct{} + +// FailureMessage is a stub to honor omegaMatcher interface +func (m *fakeOmegaMatcher) FailureMessage(_ interface{}) (message string) { + return "" +} + +// NegatedFailureMessage is a stub to honor omegaMatcher interface +func (m *fakeOmegaMatcher) NegatedFailureMessage(_ interface{}) (message string) { + return "" +} diff --git a/matchers/not.go b/matchers/not.go new file mode 100644 index 00000000..c27373d5 --- /dev/null +++ b/matchers/not.go @@ -0,0 +1,36 @@ +package matchers + +import ( + "encoding/json" +) + +type NotMatcher struct { + fakeOmegaMatcher + Matcher GossMatcher +} + +func Not(matcher GossMatcher) GossMatcher { + return &NotMatcher{Matcher: matcher} +} + +func (m *NotMatcher) Match(actual interface{}) (bool, error) { + success, err := m.Matcher.Match(actual) + if err != nil { + return false, err + } + return !success, nil +} + +func (m *NotMatcher) FailureResult(actual interface{}) MatcherResult { + return m.Matcher.NegatedFailureResult(actual) +} + +func (m *NotMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + return m.Matcher.FailureResult(actual) +} + +func (m *NotMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["not"] = m.Matcher + return json.Marshal(j) +} diff --git a/matchers/or.go b/matchers/or.go new file mode 100644 index 00000000..f34dc4e3 --- /dev/null +++ b/matchers/or.go @@ -0,0 +1,52 @@ +package matchers + +import ( + "encoding/json" +) + +type OrMatcher struct { + fakeOmegaMatcher + + Matchers []GossMatcher + + // state + firstSuccessfulMatcher GossMatcher +} + +func Or(ms ...GossMatcher) GossMatcher { + return &OrMatcher{Matchers: ms} +} + +func (m *OrMatcher) Match(actual interface{}) (success bool, err error) { + m.firstSuccessfulMatcher = nil + for _, matcher := range m.Matchers { + success, err := matcher.Match(actual) + if err != nil { + return false, err + } + if success { + m.firstSuccessfulMatcher = matcher + return true, nil + } + } + return false, nil +} + +func (m *OrMatcher) FailureResult(actual interface{}) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to satisfy at least one of these matchers", + Expected: m.Matchers, + } +} + +func (m *OrMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + firstSuccessfulMatcher := getUnexported(m, "firstSuccessfulMatcher") + return firstSuccessfulMatcher.(GossMatcher).NegatedFailureResult(actual) +} + +func (m *OrMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]interface{}) + j["or"] = m.Matchers + return json.Marshal(j) +} diff --git a/matchers/semver_constraint.go b/matchers/semver_constraint.go index b69039db..f95d934a 100644 --- a/matchers/semver_constraint.go +++ b/matchers/semver_constraint.go @@ -1,28 +1,30 @@ package matchers import ( + "bytes" + "encoding/json" "fmt" "reflect" "github.com/blang/semver/v4" "github.com/onsi/gomega/format" - "github.com/onsi/gomega/types" ) -func BeSemverConstraint(constraint any) types.GomegaMatcher { - return &BeSemverConstraintMatcher{ - Constraint: constraint, - } -} - type BeSemverConstraintMatcher struct { + fakeOmegaMatcher + Constraint any } -func (matcher *BeSemverConstraintMatcher) Match(actual any) (success bool, err error) { - constraint, ok := toConstraint(matcher.Constraint) +func BeSemverConstraint(constraint any) GossMatcher { + return &BeSemverConstraintMatcher{ + Constraint: constraint, + } +} +func (m *BeSemverConstraintMatcher) Match(actual any) (success bool, err error) { + constraint, ok := toConstraint(m.Constraint) if !ok { - return false, fmt.Errorf("Expected a valid semver constraint. Got:\n%s", format.Object(matcher.Constraint, 1)) + return false, fmt.Errorf("Expected a valid semver constraint. Got:\n%s", format.Object(m.Constraint, 1)) } actualSlice, ok := toVersions(actual) @@ -39,12 +41,20 @@ func (matcher *BeSemverConstraintMatcher) Match(actual any) (success bool, err e return true, nil } -func (matcher *BeSemverConstraintMatcher) FailureMessage(actual any) (message string) { - return format.Message(actual, fmt.Sprintf("to be %s", matcher.Constraint)) +func (m *BeSemverConstraintMatcher) FailureResult(actual any) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "to satisfy semver constraint", + Expected: m.Constraint, + } } -func (matcher *BeSemverConstraintMatcher) NegatedFailureMessage(actual any) (message string) { - return format.Message(actual, fmt.Sprintf("not to be %s", matcher.Constraint)) +func (m *BeSemverConstraintMatcher) NegatedFailureResult(actual any) MatcherResult { + return MatcherResult{ + Actual: actual, + Message: "not to satisfy semver constraint", + Expected: m.Constraint, + } } func toConstraint(in any) (semver.Range, bool) { @@ -102,3 +112,17 @@ func toVersions(in any) ([]*semver.Version, bool) { return out, len(out) > 0 } + +func (m *BeSemverConstraintMatcher) MarshalJSON() ([]byte, error) { + j := make(map[string]any) + j["semver-constraint"] = m.Constraint + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + err := encoder.Encode(j) + if err != nil { + return nil, nil + } + b := buffer.Bytes() + return b, nil +} diff --git a/matchers/semver_constraint_test.go b/matchers/semver_constraint_test.go index 4dfaef4d..db5e8e1d 100644 --- a/matchers/semver_constraint_test.go +++ b/matchers/semver_constraint_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/blang/semver/v4" - "github.com/onsi/gomega/types" ) func TestBeSemverConstraint(t *testing.T) { @@ -17,7 +16,7 @@ func TestBeSemverConstraint(t *testing.T) { tests := []struct { name string args args - want types.GomegaMatcher + want GossMatcher }{ { name: "sanity", @@ -42,22 +41,20 @@ func TestBeSemverConstraintMatcher_FailureMessage(t *testing.T) { actual any } tests := []struct { - name string - fields fields - args args - wantMessage string + name string + fields fields + args args + wantResult MatcherResult }{ { - name: "string", - fields: fields{Constraint: "> 1.1.0"}, - args: args{actual: "1.0.0"}, - wantMessage: "Expected\n : 1.0.0\nto be > 1.1.0", - }, - { - name: "slice_string", - fields: fields{Constraint: "> 1.1.0"}, - args: args{actual: []string{"1.0.0"}}, - wantMessage: "Expected\n <[]string | len:1, cap:1>: [\"1.0.0\"]\nto be > 1.1.0", + name: "string", + fields: fields{Constraint: "> 1.1.0"}, + args: args{actual: "1.0.0"}, + wantResult: MatcherResult{ + Actual: "1.0.0", + Message: "to satisfy semver constraint", + Expected: "> 1.1.0", + }, }, } for _, tt := range tests { @@ -65,8 +62,8 @@ func TestBeSemverConstraintMatcher_FailureMessage(t *testing.T) { matcher := &BeSemverConstraintMatcher{ Constraint: tt.fields.Constraint, } - gotMessage := matcher.FailureMessage(tt.args.actual) - assert.Equal(t, tt.wantMessage, gotMessage) + gotResult := matcher.FailureResult(tt.args.actual) + assert.Equal(t, tt.wantResult, gotResult) }) } } @@ -171,22 +168,20 @@ func TestBeSemverConstraintMatcher_NegatedFailureMessage(t *testing.T) { actual any } tests := []struct { - name string - fields fields - args args - wantMessage string + name string + fields fields + args args + wantResult MatcherResult }{ { - name: "string", - fields: fields{Constraint: "> 1.1.0"}, - args: args{actual: "1.0.0"}, - wantMessage: "Expected\n : 1.0.0\nnot to be > 1.1.0", - }, - { - name: "slice_string", - fields: fields{Constraint: "> 1.1.0"}, - args: args{actual: []string{"1.0.0"}}, - wantMessage: "Expected\n <[]string | len:1, cap:1>: [\"1.0.0\"]\nnot to be > 1.1.0", + name: "string", + fields: fields{Constraint: "> 1.1.0"}, + args: args{actual: "1.0.0"}, + wantResult: MatcherResult{ + Actual: "1.0.0", + Message: "not to satisfy semver constraint", + Expected: "> 1.1.0", + }, }, } for _, tt := range tests { @@ -195,8 +190,8 @@ func TestBeSemverConstraintMatcher_NegatedFailureMessage(t *testing.T) { Constraint: tt.fields.Constraint, } - gotMessage := matcher.NegatedFailureMessage(tt.args.actual) - assert.Equal(t, tt.wantMessage, gotMessage) + gotResult := matcher.NegatedFailureResult(tt.args.actual) + assert.Equal(t, tt.wantResult, gotResult) }) } } diff --git a/matchers/type_conversion.go b/matchers/type_conversion.go new file mode 100644 index 00000000..0d425bb4 --- /dev/null +++ b/matchers/type_conversion.go @@ -0,0 +1,144 @@ +package matchers + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strconv" + "strings" + + "github.com/onsi/gomega/format" + "github.com/tidwall/gjson" +) + +type Transformer interface { + Transform(interface{}) (interface{}, error) +} + +type ToNumeric struct{} + +func (t ToNumeric) Transform(e interface{}) (interface{}, error) { + switch v := e.(type) { + case float64, int: + return v, nil + case string: + return strconv.ParseFloat(strings.TrimSpace(v), 64) + case []string: + i, err := ToString{}.Transform(v) + if err != nil { + return 0, err + } + s := i.(string) + return strconv.ParseFloat(strings.TrimSpace(s), 64) + default: + return 0, fmt.Errorf("Expected numeric, Got:%s", format.Object(e, 1)) + + } +} +func (t ToNumeric) MarshalJSON() ([]byte, error) { + j := map[string]interface{}{ + "to-numeric": map[string]string{}, + } + return json.Marshal(j) +} + +type ToString struct{} + +func (t ToString) Transform(e interface{}) (interface{}, error) { + switch v := e.(type) { + case []interface{}: + vs := make([]string, len(v)) + for i, v := range v { + vs[i] = fmt.Sprintf("%v", v) + } + return strings.Join(vs, "\n"), nil + case []string: + return strings.Join(v, "\n"), nil + default: + return fmt.Sprintf("%v", v), nil + } +} + +func (t ToString) MarshalJSON() ([]byte, error) { + j := map[string]interface{}{ + "to-string": map[string]string{}, + } + return json.Marshal(j) +} + +type ToArray struct{} + +func (t ToArray) Transform(i interface{}) (interface{}, error) { + switch v := i.(type) { + case string: + return strings.Split(v, "\n"), nil + default: + return i, nil + } +} +func (matcher ToArray) MarshalJSON() ([]byte, error) { + j := map[string]interface{}{ + "to-array": map[string]string{}, + } + return json.Marshal(j) +} + +//type ReaderToStrings struct{} +// +//func (t ReaderToStrings) Transform(i interface{}) (interface{}, error) { +// r, ok := i.(io.Reader) +// if !ok { +// return nil, fmt.Errorf("Expected io.reader, Got:%s", format.Object(i, 1)) +// } +// var lines []string +// i, err := ReaderToString{}.Transform(r) +// if err != nil { +// return lines, err +// } +// s := i.(string) +// return strings.Split(s, "\n"), nil +//} + +type ReaderToString struct{} + +func (t ReaderToString) Transform(i interface{}) (interface{}, error) { + r, ok := i.(io.Reader) + if !ok { + return nil, fmt.Errorf("Expected io.reader, Got:%s", format.Object(i, 1)) + } + + b, err := ioutil.ReadAll(r) + if err != nil { + return "", err + } + return string(b), nil +} + +type Gjson struct { + Path string +} + +func (g Gjson) Transform(i interface{}) (interface{}, error) { + s, ok := i.(string) + if !ok { + return nil, fmt.Errorf("Expected string, Got:%s", format.Object(i, 1)) + } + if !gjson.Valid(s) { + return nil, fmt.Errorf("Invalid json") + } + r := gjson.Get(s, g.Path) + if !r.Exists() { + return nil, fmt.Errorf("Path not found: %s", g.Path) + } + + return r.Value(), nil +} +func (g Gjson) MarshalJSON() ([]byte, error) { + j := map[string]interface{}{ + "gjson": map[string]string{ + "Path": g.Path, + }, + } + return json.Marshal(j) +} diff --git a/matchers/with_safe_transform.go b/matchers/with_safe_transform.go new file mode 100644 index 00000000..0629c141 --- /dev/null +++ b/matchers/with_safe_transform.go @@ -0,0 +1,82 @@ +package matchers + +import ( + "encoding/json" + "fmt" + "reflect" +) + +type WithSafeTransformMatcher struct { + fakeOmegaMatcher + + // input + Transform Transformer // must be a function of one parameter that returns one value + Matcher GossMatcher + + // state + transformedValue interface{} + wasTransformed bool +} + +func WithSafeTransform(transform Transformer, matcher GossMatcher) GossMatcher { + + return &WithSafeTransformMatcher{ + Transform: transform, + Matcher: matcher, + } +} + +func (m *WithSafeTransformMatcher) Match(actual interface{}) (bool, error) { + var err error + //log.Printf("%#v: input: %v", m.Transform, actual) + m.transformedValue, err = m.Transform.Transform(actual) + if !reflect.DeepEqual(actual, m.transformedValue) { + m.wasTransformed = true + } + if err != nil { + return false, fmt.Errorf("%#v: %s", m.Transform, err) + } + //log.Printf("%#v: output: %v", m.Transform, m.transformedValue) + return m.Matcher.Match(m.transformedValue) +} + +func (m *WithSafeTransformMatcher) FailureResult(actual interface{}) MatcherResult { + tchain, matcher, tvalue := m.getTransformerChainAndMatcher() + result := matcher.FailureResult(tvalue) + result.TransformerChain = tchain + result.UntransformedValue = actual + return result +} +func (m *WithSafeTransformMatcher) NegatedFailureResult(actual interface{}) MatcherResult { + tchain, matcher, tvalue := m.getTransformerChainAndMatcher() + result := matcher.NegatedFailureResult(tvalue) + result.TransformerChain = tchain + result.UntransformedValue = actual + return result +} + +func (m *WithSafeTransformMatcher) getTransformerChainAndMatcher() (tchain []Transformer, matcher GossMatcher, tvalue interface{}) { + matcher = m + tvalue = m.transformedValue +L: + for { + switch v := matcher.(type) { + case *WithSafeTransformMatcher: + matcher = v.Matcher + tvalue = v.transformedValue + if v.wasTransformed { + tchain = append(tchain, v.Transform) + } + default: + break L + + } + } + return tchain, matcher, tvalue + +} + +func (m *WithSafeTransformMatcher) MarshalJSON() ([]byte, error) { + _, matcher, _ := m.getTransformerChainAndMatcher() + return json.Marshal(matcher) +} diff --git a/outputs/documentation.go b/outputs/documentation.go index 758e3b00..7186ff33 100644 --- a/outputs/documentation.go +++ b/outputs/documentation.go @@ -12,12 +12,20 @@ import ( type Documentation struct{} func (r Documentation) ValidOptions() []*formatOption { - return []*formatOption{} + return []*formatOption{ + {name: foSort}, + } } func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { + includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) + + sort := util.IsValueInList(foSort, outConfig.FormatOptions) + results = getResults(results, sort) + var startTime time.Time + var endTime time.Time testCount := 0 var failedOrSkipped [][]resource.TestResult var skipped, failed int @@ -29,15 +37,21 @@ func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult, fmt.Fprint(w, header) } for _, testResult := range resultGroup { + if startTime.IsZero() || testResult.StartTime.Before(startTime) { + startTime = testResult.StartTime + } + if endTime.IsZero() || testResult.EndTime.After(endTime) { + endTime = testResult.EndTime + } switch testResult.Result { case resource.SUCCESS: - fmt.Fprintln(w, humanizeResult(testResult)) + fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw)) case resource.SKIP: - fmt.Fprintln(w, humanizeResult(testResult)) + fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw)) failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) skipped++ case resource.FAIL: - fmt.Fprintln(w, humanizeResult(testResult)) + fmt.Fprintln(w, humanizeResult(testResult, false, includeRaw)) failedOrSkippedGroup = append(failedOrSkippedGroup, testResult) failed++ } @@ -49,9 +63,9 @@ func (r Documentation) Output(w io.Writer, results <-chan []resource.TestResult, } fmt.Fprint(w, "\n\n") - fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped)) + fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw)) - fmt.Fprint(w, summary(startTime, testCount, failed, skipped)) + fmt.Fprint(w, summary(startTime, endTime, testCount, failed, skipped)) if failed > 0 { return 1 } diff --git a/outputs/json.go b/outputs/json.go index 60586377..88354b43 100644 --- a/outputs/json.go +++ b/outputs/json.go @@ -17,15 +17,22 @@ type Json struct{} func (r Json) ValidOptions() []*formatOption { return []*formatOption{ {name: foPretty}, + {name: foSort}, } } func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { var pretty bool pretty = util.IsValueInList(foPretty, outConfig.FormatOptions) + includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) + sort := util.IsValueInList(foSort, outConfig.FormatOptions) + results = getResults(results, sort) + + var startTime time.Time + var endTime time.Time color.NoColor = true testCount := 0 failed := 0 @@ -33,7 +40,13 @@ func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, var resultsOut []map[string]any for resultGroup := range results { for _, testResult := range resultGroup { - if !testResult.Successful { + if startTime.IsZero() || testResult.StartTime.Before(startTime) { + startTime = testResult.StartTime + } + if endTime.IsZero() || testResult.EndTime.After(endTime) { + endTime = testResult.EndTime + } + if testResult.Result == resource.FAIL { failed++ logTrace("WARN", "FAIL", testResult, true) } else { @@ -43,7 +56,8 @@ func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, skipped++ } m := struct2map(testResult) - m["summary-line"] = humanizeResult(testResult) + m["summary-line"] = humanizeResult(testResult, false, includeRaw) + m["summary-line-compact"] = humanizeResult(testResult, true, includeRaw) m["duration"] = int64(m["duration"].(float64)) resultsOut = append(resultsOut, m) testCount++ @@ -51,7 +65,7 @@ func (r Json) Output(w io.Writer, results <-chan []resource.TestResult, } summary := make(map[string]any) - duration := time.Since(startTime) + duration := endTime.Sub(startTime) summary["test-count"] = testCount summary["failed-count"] = failed summary["skipped-count"] = skipped diff --git a/outputs/json_oneline.go b/outputs/json_oneline.go deleted file mode 100644 index 35dee226..00000000 --- a/outputs/json_oneline.go +++ /dev/null @@ -1,59 +0,0 @@ -package outputs - -import ( - "encoding/json" - "fmt" - "io" - "time" - - "github.com/fatih/color" - "github.com/goss-org/goss/resource" - "github.com/goss-org/goss/util" -) - -type JsonOneline struct{} - -func (r JsonOneline) ValidOptions() []*formatOption { - return []*formatOption{} -} - -func (r JsonOneline) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { - - color.NoColor = true - testCount := 0 - failed := 0 - var resultsOut []map[string]any - for resultGroup := range results { - for _, testResult := range resultGroup { - if !testResult.Successful { - failed++ - } - m := struct2map(testResult) - m["summary-line"] = humanizeResult(testResult) - m["duration"] = int64(m["duration"].(float64)) - resultsOut = append(resultsOut, m) - testCount++ - } - } - - summary := make(map[string]any) - duration := time.Since(startTime) - summary["test-count"] = testCount - summary["failed-count"] = failed - summary["total-duration"] = duration - summary["summary-line"] = fmt.Sprintf("Count: %d, Failed: %d, Duration: %.3fs", testCount, failed, duration.Seconds()) - - out := make(map[string]any) - out["results"] = resultsOut - out["summary"] = summary - - j, _ := json.Marshal(out) - fmt.Fprintln(w, string(j)) - - if failed > 0 { - return 1 - } - - return 0 -} diff --git a/outputs/junit.go b/outputs/junit.go index dd0d8a82..dc547d2b 100644 --- a/outputs/junit.go +++ b/outputs/junit.go @@ -16,11 +16,17 @@ import ( type JUnit struct{} func (r JUnit) ValidOptions() []*formatOption { - return []*formatOption{} + return []*formatOption{ + {name: foSort}, + } } func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { + includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) + + sort := util.IsValueInList(foSort, outConfig.FormatOptions) + results = getResults(results, sort) color.NoColor = true var testCount, failed, skipped int @@ -31,8 +37,16 @@ func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult, var summary map[int]string summary = make(map[int]string) + var startTime time.Time + var endTime time.Time for resultGroup := range results { for _, testResult := range resultGroup { + if startTime.IsZero() || testResult.StartTime.Before(startTime) { + startTime = testResult.StartTime + } + if endTime.IsZero() || testResult.EndTime.After(endTime) { + endTime = testResult.EndTime + } m := struct2map(testResult) duration := strconv.FormatFloat(m["duration"].(float64)/1000/1000/1000, 'f', 3, 64) summary[testCount] = "\n" if testResult.Result == resource.FAIL { summary[testCount] += "" + - escapeString(humanizeResult2(testResult)) + + escapeString(humanizeResult(testResult, true, includeRaw)) + "\n" summary[testCount] += "" + - escapeString(humanizeResult2(testResult)) + + escapeString(humanizeResult(testResult, true, includeRaw)) + "\n\n" failed++ @@ -55,14 +69,14 @@ func (r JUnit) Output(w io.Writer, results <-chan []resource.TestResult, skipped++ } summary[testCount] += "" + - escapeString(humanizeResult2(testResult)) + + escapeString(humanizeResult(testResult, true, includeRaw)) + "\n\n" } testCount++ } } - duration := time.Since(startTime) + duration := endTime.Sub(startTime) fmt.Fprintln(w, "") fmt.Fprintf(w, "\n", diff --git a/outputs/nagios.go b/outputs/nagios.go index e5fe6bf3..d91776db 100644 --- a/outputs/nagios.go +++ b/outputs/nagios.go @@ -20,23 +20,32 @@ func (r Nagios) ValidOptions() []*formatOption { } func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { var testCount, failed, skipped int var perfdata, verbose bool perfdata = util.IsValueInList(foPerfData, outConfig.FormatOptions) verbose = util.IsValueInList(foVerbose, outConfig.FormatOptions) + includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) + var startTime time.Time + var endTime time.Time var summary map[int]string summary = make(map[int]string) for resultGroup := range results { for _, testResult := range resultGroup { + if startTime.IsZero() || testResult.StartTime.Before(startTime) { + startTime = testResult.StartTime + } + if endTime.IsZero() || testResult.EndTime.After(endTime) { + endTime = testResult.EndTime + } switch testResult.Result { case resource.FAIL: if util.IsValueInList(foVerbose, outConfig.FormatOptions) { - summary[failed] = "Fail " + strconv.Itoa(failed+1) + " - " + humanizeResult2(testResult) + "\n" + summary[failed] = "Fail " + strconv.Itoa(failed+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n" } failed++ case resource.SKIP: @@ -46,7 +55,7 @@ func (r Nagios) Output(w io.Writer, results <-chan []resource.TestResult, } } - duration := time.Since(startTime) + duration := endTime.Sub(startTime) if failed > 0 { fmt.Fprintf(w, "GOSS CRITICAL - Count: %d, Failed: %d, Skipped: %d, Duration: %.3fs", testCount, failed, skipped, duration.Seconds()) if perfdata { diff --git a/outputs/outputs.go b/outputs/outputs.go index c3c11595..c4e8d2ec 100644 --- a/outputs/outputs.go +++ b/outputs/outputs.go @@ -1,16 +1,22 @@ package outputs import ( + "bytes" + "encoding/json" "fmt" "io" + "reflect" + "regexp" "sort" "strings" "sync" "time" + "unicode" "github.com/fatih/color" "github.com/goss-org/goss/resource" "github.com/goss-org/goss/util" + "github.com/icza/dyno" ) type formatOption struct { @@ -18,7 +24,7 @@ type formatOption struct { } type Outputer interface { - Output(io.Writer, <-chan []resource.TestResult, time.Time, util.OutputConfig) int + Output(io.Writer, <-chan []resource.TestResult, util.OutputConfig) int ValidOptions() []*formatOption } @@ -26,7 +32,6 @@ var ( outputersMu sync.Mutex outputers = map[string]Outputer{ "documentation": &Documentation{}, - "json_oneline": &JsonOneline{}, "json": &Json{}, "junit": &JUnit{}, "nagios": &Nagios{}, @@ -36,68 +41,110 @@ var ( "tap": &Tap{}, "silent": &Silent{}, } - foPerfData = "perfdata" - foVerbose = "verbose" - foPretty = "pretty" + foPerfData = "perfdata" + foVerbose = "verbose" + foPretty = "pretty" + foExcludeRaw = "exclude_raw" + foSort = "sort" ) var green = color.New(color.FgGreen).SprintfFunc() var red = color.New(color.FgRed).SprintfFunc() var yellow = color.New(color.FgYellow).SprintfFunc() +var multiple_space = regexp.MustCompile(`\s+`) -func humanizeResult(r resource.TestResult) string { - if r.Err != nil { - return red("%s: %s: Error: %s", r.ResourceId, r.Property, r.Err) +func humanizeResult(r resource.TestResult, compact bool, includeRaw bool) string { + sep := "\n" + if compact { + sep = " " } switch r.Result { case resource.SUCCESS: - return green("%s: %s: %s: matches expectation: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected) + return green("%s: %s: %s: %s: %s", r.ResourceType, r.ResourceId, r.Property, r.MatcherResult.Message, prettyPrint(r.MatcherResult.Expected, false)) + case resource.FAIL: + matcherResult := prettyPrintTestResult(r, compact, includeRaw) + return red("%s: %s: %s:%s%s", r.ResourceType, r.ResourceId, r.Property, sep, matcherResult) case resource.SKIP: return yellow("%s: %s: %s: skipped", r.ResourceType, r.ResourceId, r.Property) - case resource.FAIL: - if r.Human != "" { - return red("%s: %s: %s:\n%s", r.ResourceType, r.ResourceId, r.Property, r.Human) - } - return humanizeResult2(r) default: panic(fmt.Sprintf("Unexpected Result Code: %v\n", r.Result)) } } -func humanizeResult2(r resource.TestResult) string { - if r.Err != nil { - return red("%s: %s: Error: %s", r.ResourceId, r.Property, r.Err) +func prettyPrintTestResult(t resource.TestResult, compact bool, includeRaw bool) string { + sep := "\n" + if compact { + sep = " " } - - switch r.Result { - case resource.SUCCESS: - switch r.TestType { - case resource.Value: - return green("%s: %s: %s: matches expectation: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected) - case resource.Values: - return green("%s: %s: %s: all expectations found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(r.Expected, ", ")) - case resource.Contains: - return green("%s: %s: %s: all expectations found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(r.Expected, ", ")) - default: - return red("Unexpected type %d", r.TestType) + m := t.MatcherResult + var ss []string + //var s string + if t.Err != nil { + e := fmt.Sprint(t.Err) + if compact { + e = multiple_space.ReplaceAllString(e, " ") + } else { + e = indentLines(e) } - case resource.FAIL: - switch r.TestType { - case resource.Value: - return red("%s: %s: %s: doesn't match, expect: %s found: %s", r.ResourceType, r.ResourceId, r.Property, r.Expected, r.Found) - case resource.Values: - return red("%s: %s: %s: expectations not found [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(subtractSlice(r.Expected, r.Found), ", ")) - case resource.Contains: - return red("%s: %s: %s: patterns not found: [%s]", r.ResourceType, r.ResourceId, r.Property, strings.Join(subtractSlice(r.Expected, r.Found), ", ")) - default: - return red("Unexpected type %d", r.TestType) + ss = append(ss, "Error") + ss = append(ss, e) + } else { + ss = append(ss, "Expected") + ss = append(ss, prettyPrint(m.Actual, !compact)) + ss = append(ss, m.Message) + ss = append(ss, prettyPrint(m.Expected, !compact)) + } + + if reflect.ValueOf(m.MissingElements).IsValid() && !reflect.ValueOf(m.MissingElements).IsNil() { + ss = append(ss, "the missing elements were") + ss = append(ss, prettyPrint(m.MissingElements, !compact)) + } + if reflect.ValueOf(m.ExtraElements).IsValid() && !reflect.ValueOf(m.ExtraElements).IsNil() { + ss = append(ss, "the extra elements were") + ss = append(ss, prettyPrint(m.ExtraElements, !compact)) + } + if len(m.TransformerChain) != 0 { + ss = append(ss, "the transform chain was") + ss = append(ss, prettyPrint(m.TransformerChain, !compact)) + if includeRaw { + ss = append(ss, "the raw value was") + ss = append(ss, prettyPrint(m.UntransformedValue, !compact)) } - case resource.SKIP: - return yellow("%s: %s: %s: skipped", r.ResourceType, r.ResourceId, r.Property) - default: - panic(fmt.Sprintf("Unexpected Result Code: %v\n", r.Result)) } + return strings.Join(ss, sep) +} + +func prettyPrint(i interface{}, indent bool) string { + // JSON doesn't like non-string keys + i = dyno.ConvertMapI2MapS(i) + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + var b []byte + err := encoder.Encode(i) + if err == nil { + b = buffer.Bytes() + } else { + // FIXME: Is this the right thing to do? + b = []byte(err.Error()) + } + b = bytes.TrimRightFunc(b, unicode.IsSpace) + if indent { + return indentLines(string(b)) + } else { + return string(b) + } +} + +// indents a block of text with an indent string +func indentLines(text string) string { + indent := " " + result := "" + for _, j := range strings.Split(strings.TrimRight(text, "\n"), "\n") { + result += indent + j + "\n" + } + return result[:len(result)-1] } func RegisterOutputer(name string, outputer Outputer) { @@ -161,24 +208,6 @@ func GetOutputer(name string) (Outputer, error) { return outputers[name], nil } -func subtractSlice(x, y []string) []string { - m := make(map[string]bool) - - for _, y := range y { - m[y] = true - } - - var ret []string - for _, x := range x { - if m[x] { - continue - } - ret = append(ret, x) - } - - return ret -} - func header(t resource.TestResult) string { var out string if t.Title != "" { @@ -199,9 +228,9 @@ func header(t resource.TestResult) string { return out } -func summary(startTime time.Time, count, failed, skipped int) string { +func summary(startTime, endTime time.Time, count, failed, skipped int) string { var s string - s += fmt.Sprintf("Total Duration: %.3fs\n", time.Since(startTime).Seconds()) + s += fmt.Sprintf("Total Duration: %.3fs\n", endTime.Sub(startTime).Seconds()) f := green if failed > 0 { f = red @@ -210,10 +239,13 @@ func summary(startTime time.Time, count, failed, skipped int) string { return s } -func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult) string { +func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult, includeRaw bool) string { var s string if len(failedOrSkipped) > 0 { s += fmt.Sprint("Failures/Skipped:\n\n") + sort.Slice(failedOrSkipped, func(i, j int) bool { + return failedOrSkipped[i][0].SortKey() < failedOrSkipped[j][0].SortKey() + }) for _, failedGroup := range failedOrSkipped { first := failedGroup[0] header := header(first) @@ -221,10 +253,35 @@ func failedOrSkippedSummary(failedOrSkipped [][]resource.TestResult) string { s += fmt.Sprint(header) } for _, testResult := range failedGroup { - s += fmt.Sprintln(humanizeResult(testResult)) + s += fmt.Sprintln(humanizeResult(testResult, false, includeRaw)) } s += fmt.Sprint("\n") } } return s } + +func getResults(tr <-chan []resource.TestResult, doSort bool) <-chan []resource.TestResult { + if !doSort { + return tr + } + str := make([][]resource.TestResult, 0) + for i := range tr { + str = append(str, i) + } + + sort.Slice(str, func(i, j int) bool { + return str[i][0].SortKey() < str[j][0].SortKey() + }) + + c := make(chan []resource.TestResult) + go func(c chan []resource.TestResult) { + defer close(c) + + for _, i := range str { + c <- i + } + }(c) + + return c +} diff --git a/outputs/outputs_test.go b/outputs/outputs_test.go index b5171579..141722ed 100644 --- a/outputs/outputs_test.go +++ b/outputs/outputs_test.go @@ -41,13 +41,12 @@ func TestOutputFormatOptions(t *testing.T) { assert.Contains(t, list, foPerfData) assert.Contains(t, list, foPretty) assert.Contains(t, list, foVerbose) - assert.Len(t, list, 3) + assert.Len(t, list, 4) } func TestOptionsRegistration(t *testing.T) { registeredOutputs := Outputers() assert.Contains(t, registeredOutputs, "documentation") - assert.Contains(t, registeredOutputs, "json_oneline") assert.Contains(t, registeredOutputs, "json") assert.Contains(t, registeredOutputs, "junit") assert.Contains(t, registeredOutputs, "nagios") diff --git a/outputs/prometheus.go b/outputs/prometheus.go index be9c51b9..21be45ec 100644 --- a/outputs/prometheus.go +++ b/outputs/prometheus.go @@ -73,10 +73,14 @@ func (r Prometheus) ValidOptions() []*formatOption { // Output converts the results into the prometheus text-format. func (r Prometheus) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { overallOutcome := resource.OutcomeUnknown + var startTime time.Time for resultGroup := range results { for _, tr := range resultGroup { + if startTime.IsZero() || tr.StartTime.Before(startTime) { + startTime = tr.StartTime + } resType := strings.ToLower(tr.ResourceType) outcome := tr.ToOutcome() testOutcomes.WithLabelValues(resType, outcome).Inc() diff --git a/outputs/prometheus_test.go b/outputs/prometheus_test.go index 82a785dc..3a4689c8 100644 --- a/outputs/prometheus_test.go +++ b/outputs/prometheus_test.go @@ -37,7 +37,7 @@ func TestPrometheusOutput(t *testing.T) { }, } - exitCode := outputer.Output(buf, makeResults(injectedResults...), time.Now().Add(-1*time.Minute), util.OutputConfig{}) + exitCode := outputer.Output(buf, makeResults(injectedResults...), util.OutputConfig{}) assert.Equal(t, 0, exitCode) output := buf.String() diff --git a/outputs/rspecish.go b/outputs/rspecish.go index 84f7e20c..8c340260 100644 --- a/outputs/rspecish.go +++ b/outputs/rspecish.go @@ -18,14 +18,28 @@ func (r Rspecish) ValidOptions() []*formatOption { } func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { + sort := util.IsValueInList(foSort, outConfig.FormatOptions) + results = getResults(results, sort) + + var startTime time.Time + var endTime time.Time testCount := 0 var failedOrSkipped [][]resource.TestResult var skipped, failed int for resultGroup := range results { failedOrSkippedGroup := []resource.TestResult{} for _, testResult := range resultGroup { + // Calculates the start and end times based on the start of the first test + // and end of the last test, this allows the time/duration to be stable + // FIXME: move this to shared code + if startTime.IsZero() || testResult.StartTime.Before(startTime) { + startTime = testResult.StartTime + } + if endTime.IsZero() || testResult.EndTime.After(endTime) { + endTime = testResult.EndTime + } switch testResult.Result { case resource.SUCCESS: logTrace("TRACE", "SUCCESS", testResult, false) @@ -49,9 +63,11 @@ func (r Rspecish) Output(w io.Writer, results <-chan []resource.TestResult, } fmt.Fprint(w, "\n\n") - fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped)) + includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) + + fmt.Fprint(w, failedOrSkippedSummary(failedOrSkipped, includeRaw)) - outstr := summary(startTime, testCount, failed, skipped) + outstr := summary(startTime, endTime, testCount, failed, skipped) fmt.Fprint(w, outstr) resstr := strings.ReplaceAll(outstr, "\n", " ") if failed > 0 { diff --git a/outputs/silent.go b/outputs/silent.go index 746a4b06..eaaf94c6 100644 --- a/outputs/silent.go +++ b/outputs/silent.go @@ -2,7 +2,6 @@ package outputs import ( "io" - "time" "github.com/goss-org/goss/resource" "github.com/goss-org/goss/util" @@ -15,7 +14,7 @@ func (r Silent) ValidOptions() []*formatOption { } func (r Silent) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { var failed int for resultGroup := range results { diff --git a/outputs/structured.go b/outputs/structured.go index 146b89f0..ce988e68 100644 --- a/outputs/structured.go +++ b/outputs/structured.go @@ -16,13 +16,15 @@ type Structured struct{} func (r Structured) ValidOptions() []*formatOption { return []*formatOption{ {name: foPretty}, + {name: foSort}, } } // StructuredTestResult is an individual test result with additional human friendly summary type StructuredTestResult struct { resource.TestResult - SummaryLine string `json:"summary-line"` + SummaryLine string `json:"summary-line"` + SummaryLineCompact string `json:"summary-line-compact"` } // StructureTestSummary holds summary information about a test run @@ -45,20 +47,34 @@ func (s *StructureTestSummary) String() string { } // Output processes output from tests into StructuredOutput written to w as a string -func (r Structured) Output(w io.Writer, results <-chan []resource.TestResult, startTime time.Time, outConfig util.OutputConfig) (exitCode int) { +func (r Structured) Output(w io.Writer, results <-chan []resource.TestResult, outConfig util.OutputConfig) (exitCode int) { + includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) + + sort := util.IsValueInList(foSort, outConfig.FormatOptions) + results = getResults(results, sort) + result := &StructuredOutput{ Results: []StructuredTestResult{}, Summary: StructureTestSummary{}, } + var startTime time.Time + var endTime time.Time for resultGroup := range results { for _, testResult := range resultGroup { + if startTime.IsZero() || testResult.StartTime.Before(startTime) { + startTime = testResult.StartTime + } + if endTime.IsZero() || testResult.EndTime.After(endTime) { + endTime = testResult.EndTime + } r := StructuredTestResult{ - TestResult: testResult, - SummaryLine: humanizeResult(testResult), + TestResult: testResult, + SummaryLine: humanizeResult(testResult, false, includeRaw), + SummaryLineCompact: humanizeResult(testResult, true, includeRaw), } - if !testResult.Successful { + if testResult.Result == resource.FAIL { result.Summary.Failed++ } @@ -68,7 +84,7 @@ func (r Structured) Output(w io.Writer, results <-chan []resource.TestResult, st } } - result.Summary.TotalDuration = time.Since(startTime) + result.Summary.TotalDuration = endTime.Sub(startTime) result.SummaryLine = result.Summary.String() var j []byte diff --git a/outputs/tap.go b/outputs/tap.go index 62dfb02a..341e2a0e 100644 --- a/outputs/tap.go +++ b/outputs/tap.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "strconv" - "time" "github.com/goss-org/goss/resource" "github.com/goss-org/goss/util" @@ -13,11 +12,17 @@ import ( type Tap struct{} func (r Tap) ValidOptions() []*formatOption { - return []*formatOption{} + return []*formatOption{ + {name: foSort}, + } } func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult, - startTime time.Time, outConfig util.OutputConfig) (exitCode int) { + outConfig util.OutputConfig) (exitCode int) { + includeRaw := !util.IsValueInList(foExcludeRaw, outConfig.FormatOptions) + + sort := util.IsValueInList(foSort, outConfig.FormatOptions) + results = getResults(results, sort) testCount := 0 failed := 0 @@ -29,12 +34,12 @@ func (r Tap) Output(w io.Writer, results <-chan []resource.TestResult, for _, testResult := range resultGroup { switch testResult.Result { case resource.SUCCESS: - summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult2(testResult) + "\n" + summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n" case resource.FAIL: - summary[testCount] = "not ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult2(testResult) + "\n" + summary[testCount] = "not ok " + strconv.Itoa(testCount+1) + " - " + humanizeResult(testResult, true, includeRaw) + "\n" failed++ case resource.SKIP: - summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - # SKIP " + humanizeResult2(testResult) + "\n" + summary[testCount] = "ok " + strconv.Itoa(testCount+1) + " - # SKIP " + humanizeResult(testResult, true, includeRaw) + "\n" default: panic(fmt.Sprintf("Unexpected Result Code: %v\n", testResult.Result)) } diff --git a/outputs/traces.go b/outputs/traces.go index 1f5ceed1..2a37d5da 100644 --- a/outputs/traces.go +++ b/outputs/traces.go @@ -14,8 +14,8 @@ func logTrace(level string, msg string, testResult resource.TestResult, withIntR testResult.ResourceType, testResult.ResourceId, testResult.Property, - testResult.Expected, - testResult.Found, + testResult.MatcherResult.Expected, + testResult.MatcherResult.Actual, testResult.Duration.Seconds(), testResult.Result, ) @@ -26,8 +26,8 @@ func logTrace(level string, msg string, testResult resource.TestResult, withIntR testResult.ResourceType, testResult.ResourceId, testResult.Property, - testResult.Expected, - testResult.Found, + testResult.MatcherResult.Expected, + testResult.MatcherResult.Actual, testResult.Duration.Seconds(), ) } diff --git a/resource/addr.go b/resource/addr.go index 49d2d723..c4d4bd8b 100644 --- a/resource/addr.go +++ b/resource/addr.go @@ -1,6 +1,7 @@ package resource import ( + "fmt" "time" "github.com/goss-org/goss/system" @@ -10,7 +11,8 @@ import ( type Addr struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Address string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + Address string `json:"address,omitempty" yaml:"address,omitempty"` LocalAddress string `json:"local-address,omitempty" yaml:"local-address,omitempty"` Reachable matcher `json:"reachable" yaml:"reachable"` Timeout int `json:"timeout" yaml:"timeout"` @@ -26,8 +28,13 @@ func init() { registerResource(AddrResourceKey, &Addr{}) } -func (a *Addr) ID() string { return a.Address } -func (a *Addr) SetID(id string) { a.Address = id } +func (a *Addr) ID() string { + if a.Address != "" && a.Address != a.id { + return fmt.Sprintf("%s: %s", a.id, a.Address) + } + return a.id +} +func (a *Addr) SetID(id string) { a.id = id } func (a *Addr) SetSkip() { a.Skip = true } func (a *Addr) TypeKey() string { return AddrResourceKey } func (a *Addr) TypeName() string { return AddResourceName } @@ -35,6 +42,12 @@ func (a *Addr) TypeName() string { return AddResourceName } // FIXME: Can this be refactored? func (a *Addr) GetTitle() string { return a.Title } func (a *Addr) GetMeta() meta { return a.Meta } +func (a *Addr) GetAddress() string { + if a.Address != "" { + return a.Address + } + return a.id +} func (a *Addr) Validate(sys *system.System) []TestResult { skip := a.Skip @@ -43,7 +56,7 @@ func (a *Addr) Validate(sys *system.System) []TestResult { a.Timeout = 500 } - sysAddr := sys.NewAddr(a.Address, sys, util.Config{Timeout: time.Duration(a.Timeout) * time.Millisecond, LocalAddress: a.LocalAddress}) + sysAddr := sys.NewAddr(a.GetAddress(), sys, util.Config{Timeout: time.Duration(a.Timeout) * time.Millisecond, LocalAddress: a.LocalAddress}) var results []TestResult results = append(results, ValidateValue(a, "reachable", a.Reachable, sysAddr.Reachable, skip)) @@ -54,7 +67,7 @@ func NewAddr(sysAddr system.Addr, config util.Config) (*Addr, error) { address := sysAddr.Address() reachable, err := sysAddr.Reachable() a := &Addr{ - Address: address, + id: address, Reachable: reachable, Timeout: config.TimeOutMilliSeconds(), LocalAddress: config.LocalAddress, diff --git a/resource/command.go b/resource/command.go index e69cb1a9..424ded63 100644 --- a/resource/command.go +++ b/resource/command.go @@ -12,15 +12,15 @@ import ( ) type Command struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Command string `json:"-" yaml:"-"` - Exec string `json:"exec,omitempty" yaml:"exec,omitempty"` - ExitStatus matcher `json:"exit-status" yaml:"exit-status"` - Stdout []string `json:"stdout" yaml:"stdout"` - Stderr []string `json:"stderr" yaml:"stderr"` - Timeout int `json:"timeout" yaml:"timeout"` - Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + id string `json:"-" yaml:"-"` + Exec string `json:"exec,omitempty" yaml:"exec,omitempty"` + ExitStatus matcher `json:"exit-status" yaml:"exit-status"` + Stdout matcher `json:"stdout" yaml:"stdout"` + Stderr matcher `json:"stderr" yaml:"stderr"` + Timeout int `json:"timeout" yaml:"timeout"` + Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` } const ( @@ -32,8 +32,8 @@ func init() { registerResource(CommandResourceKey, &Command{}) } -func (c *Command) ID() string { return c.Command } -func (c *Command) SetID(id string) { c.Command = id } +func (c *Command) ID() string { return c.id } +func (c *Command) SetID(id string) { c.id = id } func (c *Command) SetSkip() { c.Skip = true } func (c *Command) TypeKey() string { return CommandResourceKey } func (c *Command) TypeName() string { return CommandResourceName } @@ -44,7 +44,7 @@ func (c *Command) GetExec() string { if c.Exec != "" { return c.Exec } - return c.Command + return c.id } func (c *Command) Validate(sys *system.System) []TestResult { @@ -57,13 +57,13 @@ func (c *Command) Validate(sys *system.System) []TestResult { var results []TestResult sysCommand := sys.NewCommand(c.GetExec(), sys, util.Config{Timeout: time.Duration(c.Timeout) * time.Millisecond}) - cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.Command)) + cExitStatus := deprecateAtoI(c.ExitStatus, fmt.Sprintf("%s: command.exit-status", c.ID())) results = append(results, ValidateValue(c, "exit-status", cExitStatus, sysCommand.ExitStatus, skip)) - if len(c.Stdout) > 0 { - results = append(results, ValidateContains(c, "stdout", c.Stdout, sysCommand.Stdout, skip)) + if isSet(c.Stdout) { + results = append(results, ValidateValue(c, "stdout", c.Stdout, sysCommand.Stdout, skip)) } - if len(c.Stderr) > 0 { - results = append(results, ValidateContains(c, "stderr", c.Stderr, sysCommand.Stderr, skip)) + if isSet(c.Stderr) { + results = append(results, ValidateValue(c, "stderr", c.Stderr, sysCommand.Stderr, skip)) } return results } @@ -72,7 +72,7 @@ func NewCommand(sysCommand system.Command, config util.Config) (*Command, error) command := sysCommand.Command() exitStatus, err := sysCommand.ExitStatus() c := &Command{ - Command: command, + id: command, ExitStatus: exitStatus, Stdout: []string{}, Stderr: []string{}, diff --git a/resource/dns.go b/resource/dns.go index ab989b08..6ab100be 100644 --- a/resource/dns.go +++ b/resource/dns.go @@ -1,6 +1,7 @@ package resource import ( + "fmt" "strings" "time" @@ -11,7 +12,8 @@ import ( type DNS struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Host string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + Resolve string `json:"resolve,omitempty" yaml:"resolve,omitempty"` Resolveable matcher `json:"resolveable,omitempty" yaml:"resolveable,omitempty"` Resolvable matcher `json:"resolvable" yaml:"resolvable"` Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"` @@ -29,13 +31,24 @@ func init() { registerResource(DNSResourceKey, &DNS{}) } -func (d *DNS) ID() string { return d.Host } -func (d *DNS) SetID(id string) { d.Host = id } +func (d *DNS) ID() string { + if d.Resolve != "" && d.Resolve != d.id { + return fmt.Sprintf("%s: %s", d.id, d.Resolve) + } + return d.id +} +func (d *DNS) SetID(id string) { d.id = id } func (d *DNS) SetSkip() { d.Skip = true } func (d *DNS) TypeKey() string { return DNSResourceKey } func (d *DNS) TypeName() string { return DNSResourceName } func (d *DNS) GetTitle() string { return d.Title } func (d *DNS) GetMeta() meta { return d.Meta } +func (d *DNS) GetResolve() string { + if d.Resolve != "" { + return d.Resolve + } + return d.id +} func (d *DNS) Validate(sys *system.System) []TestResult { skip := d.Skip @@ -43,7 +56,7 @@ func (d *DNS) Validate(sys *system.System) []TestResult { d.Timeout = 500 } - sysDNS := sys.NewDNS(d.Host, sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server}) + sysDNS := sys.NewDNS(d.GetResolve(), sys, util.Config{Timeout: time.Duration(d.Timeout) * time.Millisecond, Server: d.Server}) var results []TestResult // Backwards compatibility hack for now @@ -72,7 +85,7 @@ func NewDNS(sysDNS system.DNS, config util.Config) (*DNS, error) { server := sysDNS.Server() d := &DNS{ - Host: host, + id: host, Resolvable: resolvable, Timeout: config.TimeOutMilliSeconds(), Server: server, diff --git a/resource/file.go b/resource/file.go index 3b0e207e..2d2fa66c 100644 --- a/resource/file.go +++ b/resource/file.go @@ -1,26 +1,31 @@ package resource import ( + "fmt" + "os" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) type File struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Path string `json:"-" yaml:"-"` - Exists matcher `json:"exists" yaml:"exists"` - Mode matcher `json:"mode,omitempty" yaml:"mode,omitempty"` - Size matcher `json:"size,omitempty" yaml:"size,omitempty"` - Owner matcher `json:"owner,omitempty" yaml:"owner,omitempty"` - Group matcher `json:"group,omitempty" yaml:"group,omitempty"` - LinkedTo matcher `json:"linked-to,omitempty" yaml:"linked-to,omitempty"` - Filetype matcher `json:"filetype,omitempty" yaml:"filetype,omitempty"` - Contains []string `json:"contains" yaml:"contains"` - Md5 matcher `json:"md5,omitempty" yaml:"md5,omitempty"` - Sha256 matcher `json:"sha256,omitempty" yaml:"sha256,omitempty"` - Sha512 matcher `json:"sha512,omitempty" yaml:"sha512,omitempty"` - Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + id string `json:"-" yaml:"-"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Exists matcher `json:"exists" yaml:"exists"` + Mode matcher `json:"mode,omitempty" yaml:"mode,omitempty"` + Size matcher `json:"size,omitempty" yaml:"size,omitempty"` + Owner matcher `json:"owner,omitempty" yaml:"owner,omitempty"` + Group matcher `json:"group,omitempty" yaml:"group,omitempty"` + LinkedTo matcher `json:"linked-to,omitempty" yaml:"linked-to,omitempty"` + Filetype matcher `json:"filetype,omitempty" yaml:"filetype,omitempty"` + Contains matcher `json:"contains,omitempty" yaml:"contains,omitempty"` + Contents matcher `json:"contents" yaml:"contents"` + Md5 matcher `json:"md5,omitempty" yaml:"md5,omitempty"` + Sha256 matcher `json:"sha256,omitempty" yaml:"sha256,omitempty"` + Sha512 matcher `json:"sha512,omitempty" yaml:"sha512,omitempty"` + Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` } const ( @@ -32,18 +37,29 @@ func init() { registerResource(FileResourceKey, &File{}) } -func (f *File) ID() string { return f.Path } -func (f *File) SetID(id string) { f.Path = id } +func (f *File) ID() string { + if f.Path != "" && f.Path != f.id { + return fmt.Sprintf("%s: %s", f.id, f.Path) + } + return f.id +} +func (f *File) SetID(id string) { f.id = id } func (f *File) SetSkip() { f.Skip = true } func (f *File) TypeKey() string { return FileResourceKey } func (f *File) TypeName() string { return FileResourceName } func (f *File) GetTitle() string { return f.Title } func (f *File) GetMeta() meta { return f.Meta } +func (f *File) GetPath() string { + if f.Path != "" { + return f.Path + } + return f.id +} func (f *File) Validate(sys *system.System) []TestResult { skip := f.Skip - sysFile := sys.NewFile(f.Path, sys, util.Config{}) + sysFile := sys.NewFile(f.GetPath(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(f, "exists", f.Exists, sysFile.Exists, skip)) @@ -65,8 +81,12 @@ func (f *File) Validate(sys *system.System) []TestResult { if f.Filetype != nil { results = append(results, ValidateValue(f, "filetype", f.Filetype, sysFile.Filetype, skip)) } - if len(f.Contains) > 0 { - results = append(results, ValidateContains(f, "contains", f.Contains, sysFile.Contains, skip)) + if isSet(f.Contains) { + fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: file.contains has been renamed to file.contents\n") + results = append(results, ValidateValue(f, "contains", f.Contains, sysFile.Contents, skip)) + } + if isSet(f.Contents) { + results = append(results, ValidateValue(f, "contents", f.Contents, sysFile.Contents, skip)) } if f.Size != nil { results = append(results, ValidateValue(f, "size", f.Size, sysFile.Size, skip)) @@ -90,9 +110,9 @@ func NewFile(sysFile system.File, config util.Config) (*File, error) { return nil, err } f := &File{ - Path: path, + id: path, Exists: exists, - Contains: []string{}, + Contents: []string{}, } if !contains(config.IgnoreList, "mode") { if mode, err := sysFile.Mode(); err == nil { @@ -119,10 +139,5 @@ func NewFile(sysFile system.File, config util.Config) (*File, error) { f.Filetype = filetype } } - if !contains(config.IgnoreList, "size") { - if size, err := sysFile.Size(); err == nil { - f.Size = size - } - } return f, nil } diff --git a/resource/gomega.go b/resource/gomega.go index 84bc7596..97da1026 100644 --- a/resource/gomega.go +++ b/resource/gomega.go @@ -2,28 +2,29 @@ package resource import ( "fmt" - "sort" "github.com/goss-org/goss/matchers" - - "github.com/onsi/gomega" - "github.com/onsi/gomega/types" ) -func matcherToGomegaMatcher(matcher any) (types.GomegaMatcher, error) { +func matcherToGomegaMatcher(matcher any) (matchers.GossMatcher, error) { + // Default matchers switch x := matcher.(type) { - case string, int, bool, float64: - return gomega.Equal(x), nil + case string: + return matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal(x)), nil + case float64, int: + return matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("eq", x)), nil + case bool: + return matchers.Equal(x), nil case []any: - var matchers []types.GomegaMatcher - for _, valueI := range x { - if subMatcher, ok := valueI.(types.GomegaMatcher); ok { - matchers = append(matchers, subMatcher) - } else { - matchers = append(matchers, gomega.ContainElement(valueI)) - } + subMatchers, err := sliceToGomega(x) + if err != nil { + return nil, err + } + var interfaceSlice []any + for _, d := range subMatchers { + interfaceSlice = append(interfaceSlice, d) } - return gomega.And(matchers...), nil + return matchers.ContainElements(interfaceSlice...), nil } matcher = sanitizeExpectedValue(matcher) if matcher == nil { @@ -39,44 +40,77 @@ func matcherToGomegaMatcher(matcher any) (types.GomegaMatcher, error) { break } switch matchType { + case "equal": + return matchers.Equal(value), nil case "have-prefix": - return gomega.HavePrefix(value.(string)), nil + v, isStr := value.(string) + if !isStr { + return nil, fmt.Errorf("have-prefix: syntax error: incorrect expectation type. expected string, got: %#v", value) + } + return matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePrefix(v)), nil case "have-suffix": - return gomega.HaveSuffix(value.(string)), nil + v, isStr := value.(string) + if !isStr { + return nil, fmt.Errorf("have-suffix: syntax error: incorrect expectation type. expected string, got: %#v", value) + } + return matchers.WithSafeTransform(matchers.ToString{}, matchers.HaveSuffix(v)), nil case "match-regexp": - return gomega.MatchRegexp(value.(string)), nil + v, isStr := value.(string) + if !isStr { + return nil, fmt.Errorf("match-regexp: syntax error: incorrect expectation type. expected string, got: %#v", value) + } + return matchers.WithSafeTransform(matchers.ToString{}, matchers.MatchRegexp(v)), nil + case "contain-substring": + v, isStr := value.(string) + if !isStr { + return nil, fmt.Errorf("contain-substring: syntax error: incorrect expectation type. expected string, got: %#v", value) + } + return matchers.WithSafeTransform(matchers.ToString{}, matchers.ContainSubstring(v)), nil case "have-len": - value = sanitizeExpectedValue(value) - return gomega.HaveLen(value.(int)), nil - case "have-key-with-value": - subMatchers, err := mapToGomega(value) - if err != nil { - return nil, err + v, isFloat := value.(float64) + if !isFloat { + return nil, fmt.Errorf("have-len: syntax error: incorrect expectation type. expected numeric, got: %#v", value) } - for key, val := range subMatchers { - if val == nil { - fmt.Printf("%d is nil", key) - } + return matchers.HaveLen(int(v)), nil + case "have-patterns": + _, isArr := value.([]any) + if !isArr { + return nil, fmt.Errorf("have-patterns: syntax error: incorrect expectation type. expected array, got: %#v", value) } - return gomega.And(subMatchers...), nil + return matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePatterns(value)), nil case "have-key": subMatcher, err := matcherToGomegaMatcher(value) if err != nil { return nil, err } - return gomega.HaveKey(subMatcher), nil + return matchers.HaveKey(subMatcher), nil case "contain-element": + switch value.(type) { + case map[string]any, string, float64: + default: + return nil, fmt.Errorf("contain-element: syntax error: incorrect expectation type. expected matcher or value, got: %#v", value) + } subMatcher, err := matcherToGomegaMatcher(value) if err != nil { return nil, err } - return gomega.ContainElement(subMatcher), nil + return matchers.WithSafeTransform(matchers.ToArray{}, matchers.ContainElement(subMatcher)), nil + case "contain-elements": + subMatchers, err := sliceToGomega(value) + if err != nil { + return nil, err + } + var interfaceSlice []any + for _, d := range subMatchers { + interfaceSlice = append(interfaceSlice, d) + } + return matchers.WithSafeTransform(matchers.ToArray{}, matchers.ContainElements(interfaceSlice...)), nil case "not": subMatcher, err := matcherToGomegaMatcher(value) if err != nil { return nil, err } - return gomega.Not(subMatcher), nil + return matchers.Not(subMatcher), nil case "consist-of": subMatchers, err := sliceToGomega(value) if err != nil { @@ -86,70 +120,55 @@ func matcherToGomegaMatcher(matcher any) (types.GomegaMatcher, error) { for _, d := range subMatchers { interfaceSlice = append(interfaceSlice, d) } - return gomega.ConsistOf(interfaceSlice...), nil + return matchers.ConsistOf(interfaceSlice...), nil case "and": subMatchers, err := sliceToGomega(value) if err != nil { return nil, err } - return gomega.And(subMatchers...), nil + return matchers.And(subMatchers...), nil case "or": subMatchers, err := sliceToGomega(value) if err != nil { return nil, err } - return gomega.Or(subMatchers...), nil + return matchers.Or(subMatchers...), nil case "gt", "ge", "lt", "le": - // Golang json escapes '>', '<' symbols, so we use 'gt', 'le' instead - comparator := map[string]string{ - "gt": ">", - "ge": ">=", - "lt": "<", - "le": "<=", - }[matchType] - return gomega.BeNumerically(comparator, value), nil + return matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically(matchType, value)), nil case "semver-constraint": - return matchers.BeSemverConstraint(value.(string)), nil - default: - return nil, fmt.Errorf("Unknown matcher: %s", matchType) - - } -} - -func mapToGomega(value any) (subMatchers []types.GomegaMatcher, err error) { - valueI, ok := value.(map[string]any) - if !ok { - return nil, fmt.Errorf("Matcher expected map, got: %t", value) - } + v, isStr := value.(string) + if !isStr { + return nil, fmt.Errorf("semver-contstraint: syntax error: incorrect expectation type. expected string, got: %#v", value) + } + return matchers.BeSemverConstraint(v), nil + case "gjson": + var subMatchers []matchers.GossMatcher + valueI, ok := value.(map[string]any) + if !ok { + return nil, fmt.Errorf("Matcher expected map, got: %t", value) + } + for key, val := range valueI { + subMatcher, err := matcherToGomegaMatcher(val) + if err != nil { + return nil, err + } + subMatchers = append(subMatchers, matchers.WithSafeTransform(matchers.Gjson{Path: key}, subMatcher)) - // Get keys - keys := []string{} - for key, _ := range valueI { - keys = append(keys, key) - } - // Iterate through keys in a deterministic way, since ranging over a map - // does not guarantee order - sort.Strings(keys) - for _, key := range keys { - val := valueI[key] - val, err = matcherToGomegaMatcher(val) - if err != nil { - return } + return matchers.And(subMatchers...), nil + default: + return nil, fmt.Errorf("Unknown matcher: %s", matchType) - subMatcher := gomega.HaveKeyWithValue(key, val) - subMatchers = append(subMatchers, subMatcher) } - return } -func sliceToGomega(value any) ([]types.GomegaMatcher, error) { +func sliceToGomega(value any) ([]matchers.GossMatcher, error) { valueI, ok := value.([]any) if !ok { return nil, fmt.Errorf("Matcher expected array, got: %t", value) } - var subMatchers []types.GomegaMatcher + var subMatchers []matchers.GossMatcher for _, v := range valueI { subMatcher, err := matcherToGomegaMatcher(v) if err != nil { @@ -160,16 +179,17 @@ func sliceToGomega(value any) ([]types.GomegaMatcher, error) { return subMatchers, nil } -// Normalize expectedValue so json and yaml are the same +// sanitizeExpectedValue normalizes the value so json and yaml are the same func sanitizeExpectedValue(i any) any { - if e, ok := i.(float64); ok { - return int(e) + if e, ok := i.(int); ok { + return float64(e) } if e, ok := i.(map[any]any); ok { out := make(map[string]any) for k, v := range e { ks, ok := k.(string) if !ok { + // We should never get here panic(fmt.Sprintf("Matcher key type not string: %T\n\n", k)) } out[ks] = sanitizeExpectedValue(v) diff --git a/resource/gomega_test.go b/resource/gomega_test.go index c1ace3f7..bccb3a41 100644 --- a/resource/gomega_test.go +++ b/resource/gomega_test.go @@ -2,15 +2,10 @@ package resource import ( "encoding/json" - "fmt" - "reflect" - "regexp" "testing" "github.com/goss-org/goss/matchers" - - "github.com/onsi/gomega" - "github.com/onsi/gomega/types" + "github.com/stretchr/testify/assert" ) var gomegaTests = []struct { @@ -21,20 +16,22 @@ var gomegaTests = []struct { // Default for simple types { in: `"foo"`, - want: gomega.Equal("foo"), + want: matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo")), }, { in: `1`, - want: gomega.Equal(float64(1)), + want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("eq", float64(1))), }, { in: `true`, - want: gomega.Equal(true), + want: matchers.Equal(true), }, // Default for Array { - in: `["foo", "bar"]`, - want: gomega.And(gomega.ContainElement("foo"), gomega.ContainElement("bar")), + in: `["foo", "bar"]`, + want: matchers.ContainElements( + matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo")), + matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("bar"))), useNegateTester: true, }, @@ -42,91 +39,103 @@ var gomegaTests = []struct { // Golang json escapes '>', '<' symbols, so we use 'gt', 'le' instead { in: `{"gt": 1}`, - want: gomega.BeNumerically(">", float64(1)), + want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("gt", float64(1))), }, { in: `{"ge": 1}`, - want: gomega.BeNumerically(">=", float64(1)), + want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("ge", float64(1))), }, { in: `{"lt": 1}`, - want: gomega.BeNumerically("<", float64(1)), + want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("lt", float64(1))), }, { in: `{"le": 1}`, - want: gomega.BeNumerically("<=", float64(1)), + want: matchers.WithSafeTransform(matchers.ToNumeric{}, matchers.BeNumerically("le", float64(1))), }, // String { in: `{"have-prefix": "foo"}`, - want: gomega.HavePrefix("foo"), + want: matchers.WithSafeTransform(matchers.ToString{}, matchers.HavePrefix("foo")), }, { in: `{"have-suffix": "foo"}`, - want: gomega.HaveSuffix("foo"), + want: matchers.WithSafeTransform(matchers.ToString{}, matchers.HaveSuffix("foo")), }, // Regex support is based on golangs regex engine https://golang.org/pkg/regexp/syntax/ { in: `{"match-regexp": "foo"}`, - want: gomega.MatchRegexp("foo"), + want: matchers.WithSafeTransform(matchers.ToString{}, matchers.MatchRegexp("foo")), }, // Collection { in: `{"consist-of": ["foo"]}`, - want: gomega.ConsistOf(gomega.Equal("foo")), + want: matchers.ConsistOf(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))), }, { - in: `{"contain-element": "foo"}`, - want: gomega.ContainElement(gomega.Equal("foo")), + in: `{"contain-element": "foo"}`, + want: matchers.WithSafeTransform(matchers.ToArray{}, + matchers.ContainElement( + matchers.WithSafeTransform(matchers.ToString{}, + matchers.Equal("foo")))), }, { in: `{"have-len": 3}`, - want: gomega.HaveLen(3), - }, - { - in: `{"have-key-with-value": { "foo": 1, "bar": "baz" }}`, - // Keys are sorted and then passed to gomega.And so the order - // of the conditions in this `want` is important - want: gomega.And( - gomega.HaveKeyWithValue("bar", gomega.Equal("baz")), - gomega.HaveKeyWithValue("foo", gomega.Equal(1)), - ), - useNegateTester: true, + want: matchers.HaveLen(3), }, { in: `{"have-key": "foo"}`, - want: gomega.HaveKey(gomega.Equal("foo")), + want: matchers.HaveKey(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))), }, // Negation { in: `{"not": "foo"}`, - want: gomega.Not(gomega.Equal("foo")), + want: matchers.Not(matchers.WithSafeTransform(matchers.ToString{}, matchers.Equal("foo"))), }, // Complex logic { - in: `{"and": ["foo", "foo"]}`, - want: gomega.And(gomega.Equal("foo"), gomega.Equal("foo")), + in: `{"and": ["foo", "foo"]}`, + want: matchers.And( + matchers.WithSafeTransform(matchers.ToString{}, + matchers.Equal("foo")), + matchers.WithSafeTransform(matchers.ToString{}, + matchers.Equal("foo")), + ), useNegateTester: true, }, { - in: `{"and": [{"have-prefix": "foo"}, "foo"]}`, - want: gomega.And(gomega.HavePrefix("foo"), gomega.Equal("foo")), + in: `{"and": [{"have-prefix": "foo"}, "foo"]}`, + want: matchers.And( + matchers.WithSafeTransform(matchers.ToString{}, + matchers.HavePrefix("foo")), + matchers.WithSafeTransform(matchers.ToString{}, + matchers.Equal("foo")), + ), useNegateTester: true, }, { - in: `{"not": {"have-prefix": "foo"}}`, - want: gomega.Not(gomega.HavePrefix("foo")), + in: `{"not": {"have-prefix": "foo"}}`, + want: matchers.Not( + matchers.WithSafeTransform(matchers.ToString{}, + matchers.HavePrefix("foo"))), }, { - in: `{"or": ["foo", "foo"]}`, - want: gomega.Or(gomega.Equal("foo"), gomega.Equal("foo")), + in: `{"or": ["foo", "foo"]}`, + want: matchers.Or( + matchers.WithSafeTransform(matchers.ToString{}, + matchers.Equal("foo")), + matchers.WithSafeTransform(matchers.ToString{}, + matchers.Equal("foo"))), }, { - in: `{"not": {"and": [{"have-prefix": "foo"}]}}`, - want: gomega.Not(gomega.And(gomega.HavePrefix("foo"))), + in: `{"not": {"and": [{"have-prefix": "foo"}]}}`, + want: matchers.Not( + matchers.And( + matchers.WithSafeTransform(matchers.ToString{}, + matchers.HavePrefix("foo")))), }, // Semver Constraint @@ -151,34 +160,5 @@ func TestMatcherToGomegaMatcher(t *testing.T) { } func gomegaTestEqual(t *testing.T, got, want any, useNegateTester bool, in string) { - if !gomegaEqual(got, want, useNegateTester) { - t.Errorf("For input '%s': got %T %v, want %T %v", in, got, got, want, want) - } -} -func gomegaEqual(g, w any, negateTester bool) bool { - gotT := reflect.TypeOf(g) - wantT := reflect.TypeOf(w) - got := g.(types.GomegaMatcher) - want := w.(types.GomegaMatcher) - var gotMessage string - var wantMessage string - if negateTester { - gotMessage = got.NegatedFailureMessage("foo") - wantMessage = want.NegatedFailureMessage("foo") - } else { - gotMessage = got.FailureMessage("foo") - wantMessage = want.FailureMessage("foo") - } - gotMessage = sanitizeMatcherText(gotMessage) - wantMessage = sanitizeMatcherText(wantMessage) - fmt.Println("got:", gotMessage) - fmt.Println("want:", wantMessage) - - return gotT == wantT && - gotMessage == wantMessage -} - -func sanitizeMatcherText(s string) string { - r := regexp.MustCompile("[0-9]x[a-z0-9]{10}") - return r.ReplaceAllString(s, "") + assert.Equal(t, got, want) } diff --git a/resource/group.go b/resource/group.go index 58c8f6a6..7142fb69 100644 --- a/resource/group.go +++ b/resource/group.go @@ -10,7 +10,8 @@ import ( type Group struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Groupname string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + Groupname string `json:"groupname,omitempty" yaml:"groupname,omitempty"` Exists matcher `json:"exists" yaml:"exists"` GID matcher `json:"gid,omitempty" yaml:"gid,omitempty"` Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` @@ -25,17 +26,28 @@ func init() { registerResource(GroupResourceKey, &Group{}) } -func (g *Group) ID() string { return g.Groupname } -func (g *Group) SetID(id string) { g.Groupname = id } +func (g *Group) ID() string { + if g.Groupname != "" && g.Groupname != g.id { + return fmt.Sprintf("%s: %s", g.id, g.Groupname) + } + return g.id +} +func (g *Group) SetID(id string) { g.id = id } func (g *Group) SetSkip() { g.Skip = true } func (g *Group) TypeKey() string { return GroupResourceKey } func (g *Group) TypeName() string { return GroupResourceName } func (g *Group) GetTitle() string { return g.Title } func (g *Group) GetMeta() meta { return g.Meta } +func (g *Group) GetGroupname() string { + if g.Groupname != "" { + return g.Groupname + } + return g.id +} func (g *Group) Validate(sys *system.System) []TestResult { skip := g.Skip - sysgroup := sys.NewGroup(g.Groupname, sys, util.Config{}) + sysgroup := sys.NewGroup(g.GetGroupname(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(g, "exists", g.Exists, sysgroup.Exists, skip)) @@ -43,7 +55,7 @@ func (g *Group) Validate(sys *system.System) []TestResult { skip = true } if g.GID != nil { - gGID := deprecateAtoI(g.GID, fmt.Sprintf("%s: group.gid", g.Groupname)) + gGID := deprecateAtoI(g.GID, fmt.Sprintf("%s: group.gid", g.ID())) results = append(results, ValidateValue(g, "gid", gGID, sysgroup.GID, skip)) } return results @@ -53,8 +65,8 @@ func NewGroup(sysGroup system.Group, config util.Config) (*Group, error) { groupname := sysGroup.Groupname() exists, _ := sysGroup.Exists() g := &Group{ - Groupname: groupname, - Exists: exists, + id: groupname, + Exists: exists, } if !contains(config.IgnoreList, "stderr") { if gid, err := sysGroup.GID(); err == nil { diff --git a/resource/http.go b/resource/http.go index b43ff06b..441636e5 100644 --- a/resource/http.go +++ b/resource/http.go @@ -1,6 +1,7 @@ package resource import ( + "fmt" "time" "github.com/goss-org/goss/system" @@ -9,18 +10,18 @@ import ( type HTTP struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` - URL string `json:"url,omitempty" yaml:"url,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - HTTP string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` Method string `json:"method,omitempty" yaml:"method,omitempty"` Status matcher `json:"status" yaml:"status"` AllowInsecure bool `json:"allow-insecure" yaml:"allow-insecure"` NoFollowRedirects bool `json:"no-follow-redirects" yaml:"no-follow-redirects"` - Timeout int `json:"timeout" yaml:"timeout"` + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` RequestHeader []string `json:"request-headers,omitempty" yaml:"request-headers,omitempty"` - RequestBody string `json:"request-body,omitemptyy" yaml:"request-body,omitempty"` - Headers []string `json:"headers,omitempty" yaml:"headers,omitempty"` - Body []string `json:"body" yaml:"body"` + RequestBody string `json:"request-body,omitempty" yaml:"request-body,omitempty"` + Headers matcher `json:"headers,omitempty" yaml:"headers,omitempty"` + Body matcher `json:"body,omitempty" yaml:"body,omitempty"` Username string `json:"username,omitempty" yaml:"username,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty"` CAFile string `json:"ca-file,omitempty" yaml:"ca-file,omitempty"` @@ -39,22 +40,25 @@ func init() { registerResource(HTTPResourceKey, &HTTP{}) } -func (u *HTTP) ID() string { return u.HTTP } - -func (u *HTTP) SetID(id string) { u.HTTP = id } +func (h *HTTP) ID() string { + if h.URL != "" && h.URL != h.id { + return fmt.Sprintf("%s: %s", h.id, h.URL) + } + return h.id +} +func (h *HTTP) SetID(id string) { h.id = id } func (u *HTTP) SetSkip() { u.Skip = true } func (u *HTTP) TypeKey() string { return HTTPResourceKey } func (u *HTTP) TypeName() string { return HTTPResourceName } // FIXME: Can this be refactored? -func (u *HTTP) GetTitle() string { return u.Title } -func (u *HTTP) GetMeta() meta { return u.Meta } - -func (u *HTTP) getURL() string { - if u.URL != "" { - return u.URL +func (r *HTTP) GetTitle() string { return r.Title } +func (r *HTTP) GetMeta() meta { return r.Meta } +func (r *HTTP) getURL() string { + if r.URL != "" { + return r.URL } - return u.HTTP + return r.id } func (u *HTTP) Validate(sys *system.System) []TestResult { @@ -78,11 +82,11 @@ func (u *HTTP) Validate(sys *system.System) []TestResult { if shouldSkip(results) { skip = true } - if len(u.Headers) > 0 { - results = append(results, ValidateContains(u, "Headers", u.Headers, sysHTTP.Headers, skip)) + if isSet(u.Headers) { + results = append(results, ValidateValue(u, "Headers", u.Headers, sysHTTP.Headers, skip)) } - if len(u.Body) > 0 { - results = append(results, ValidateContains(u, "Body", u.Body, sysHTTP.Body, skip)) + if isSet(u.Body) { + results = append(results, ValidateValue(u, "Body", u.Body, sysHTTP.Body, skip)) } return results @@ -92,10 +96,10 @@ func NewHTTP(sysHTTP system.HTTP, config util.Config) (*HTTP, error) { http := sysHTTP.HTTP() status, err := sysHTTP.Status() u := &HTTP{ - HTTP: http, + id: http, Status: status, RequestHeader: []string{}, - Headers: []string{}, + Headers: nil, Body: []string{}, AllowInsecure: config.AllowInsecure, NoFollowRedirects: config.NoFollowRedirects, diff --git a/resource/interface.go b/resource/interface.go index 94012408..c26cb66b 100644 --- a/resource/interface.go +++ b/resource/interface.go @@ -1,6 +1,8 @@ package resource import ( + "fmt" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) @@ -8,7 +10,8 @@ import ( type Interface struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Name string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` Exists matcher `json:"exists" yaml:"exists"` Addrs matcher `json:"addrs,omitempty" yaml:"addrs,omitempty"` MTU matcher `json:"mtu,omitempty" yaml:"mtu,omitempty"` @@ -24,8 +27,13 @@ func init() { registerResource(InterfaceResourceKey, &Interface{}) } -func (i *Interface) ID() string { return i.Name } -func (i *Interface) SetID(id string) { i.Name = id } +func (i *Interface) ID() string { + if i.Name != "" && i.Name != i.id { + return fmt.Sprintf("%s: %s", i.id, i.Name) + } + return i.id +} +func (i *Interface) SetID(id string) { i.id = id } func (i *Interface) SetSkip() { i.Skip = true } func (i *Interface) TypeKey() string { return InterfaceResourceKey } func (i *Interface) TypeName() string { return InterfaceResourceName } @@ -33,10 +41,16 @@ func (i *Interface) TypeName() string { return InterfaceResourceName } // FIXME: Can this be refactored? func (i *Interface) GetTitle() string { return i.Title } func (i *Interface) GetMeta() meta { return i.Meta } +func (i *Interface) GetName() string { + if i.Name != "" { + return i.Name + } + return i.id +} func (i *Interface) Validate(sys *system.System) []TestResult { skip := i.Skip - sysInterface := sys.NewInterface(i.Name, sys, util.Config{}) + sysInterface := sys.NewInterface(i.GetName(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(i, "exists", i.Exists, sysInterface.Exists, skip)) @@ -56,7 +70,7 @@ func NewInterface(sysInterface system.Interface, config util.Config) (*Interface name := sysInterface.Name() exists, _ := sysInterface.Exists() i := &Interface{ - Name: name, + id: name, Exists: exists, } if !contains(config.IgnoreList, "addrs") { diff --git a/resource/kernel_param.go b/resource/kernel_param.go index 4f886d96..a8e3d9f5 100644 --- a/resource/kernel_param.go +++ b/resource/kernel_param.go @@ -1,6 +1,8 @@ package resource import ( + "fmt" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) @@ -8,6 +10,8 @@ import ( type KernelParam struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + id string `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` Key string `json:"-" yaml:"-"` Value matcher `json:"value" yaml:"value"` Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` @@ -22,22 +26,34 @@ func init() { registerResource(KernelParamResourceKey, &KernelParam{}) } -func (a *KernelParam) ID() string { return a.Key } -func (a *KernelParam) SetID(id string) { a.Key = id } +func (k *KernelParam) ID() string { + if k.Name != "" && k.Name != k.id { + return fmt.Sprintf("%s: %s", k.id, k.Name) + } + return k.id +} +func (a *KernelParam) SetID(id string) { a.id = id } + func (a *KernelParam) SetSkip() { a.Skip = true } func (a *KernelParam) TypeKey() string { return KernelParamResourceKey } func (a *KernelParam) TypeName() string { return KernelParamResourceName } // FIXME: Can this be refactored? -func (a *KernelParam) GetTitle() string { return a.Title } -func (a *KernelParam) GetMeta() meta { return a.Meta } +func (k *KernelParam) GetTitle() string { return k.Title } +func (k *KernelParam) GetMeta() meta { return k.Meta } +func (k *KernelParam) GetName() string { + if k.Name != "" { + return k.Name + } + return k.id +} -func (a *KernelParam) Validate(sys *system.System) []TestResult { - skip := a.Skip - sysKernelParam := sys.NewKernelParam(a.Key, sys, util.Config{}) +func (k *KernelParam) Validate(sys *system.System) []TestResult { + skip := k.Skip + sysKernelParam := sys.NewKernelParam(k.GetName(), sys, util.Config{}) var results []TestResult - results = append(results, ValidateValue(a, "value", a.Value, sysKernelParam.Value, skip)) + results = append(results, ValidateValue(k, "value", k.Value, sysKernelParam.Value, skip)) return results } @@ -45,7 +61,7 @@ func NewKernelParam(sysKernelParam system.KernelParam, config util.Config) (*Ker key := sysKernelParam.Key() value, err := sysKernelParam.Value() a := &KernelParam{ - Key: key, + id: key, Value: value, } return a, err diff --git a/resource/matching.go b/resource/matching.go index b70bdcdf..5b608753 100644 --- a/resource/matching.go +++ b/resource/matching.go @@ -3,6 +3,7 @@ package resource import ( "encoding/json" "fmt" + "io" "reflect" "strings" @@ -11,11 +12,13 @@ import ( ) type Matching struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Content any `json:"content,omitempty" yaml:"content,omitempty"` - Id string `json:"-" yaml:"-"` - Matches matcher `json:"matches" yaml:"matches"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + Content any `json:"content,omitempty" yaml:"content,omitempty"` + AsReader bool `json:"as-reader,omitempty" yaml:"as-reader,omitempty"` + id string `json:"-" yaml:"-"` + Matches matcher `json:"matches" yaml:"matches"` + Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` } const ( @@ -25,8 +28,8 @@ const ( type MatchingMap map[string]*Matching -func (a *Matching) ID() string { return a.Id } -func (a *Matching) SetID(id string) { a.Id = id } +func (a *Matching) ID() string { return a.id } +func (a *Matching) SetID(id string) { a.id = id } func (a *Matching) SetSkip() {} func (a *Matching) TypeKey() string { return MatchingResourceKey } func (a *Matching) TypeName() string { return MatchingResourceName } @@ -37,10 +40,22 @@ func (r *Matching) GetMeta() meta { return r.Meta } func (a *Matching) Validate(sys *system.System) []TestResult { skip := false + if a.Skip { + skip = true + } - // ValidateValue expects a function - stub := func() (any, error) { - return a.Content, nil + var stub interface{} + if a.AsReader { + s := fmt.Sprintf("%v", a.Content) + // ValidateValue expects a function + stub = func() (io.Reader, error) { + return strings.NewReader(s), nil + } + } else { + // ValidateValue expects a function + stub = func() (any, error) { + return a.Content, nil + } } var results []TestResult diff --git a/resource/mount.go b/resource/mount.go index 09709362..cf8850f3 100644 --- a/resource/mount.go +++ b/resource/mount.go @@ -1,6 +1,8 @@ package resource import ( + "fmt" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) @@ -8,7 +10,8 @@ import ( type Mount struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - MountPoint string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + MountPoint string `json:"mountpoint,omitempty" yaml:"mountpoint,omitempty"` Exists matcher `json:"exists" yaml:"exists"` Opts matcher `json:"opts,omitempty" yaml:"opts,omitempty"` Source matcher `json:"source,omitempty" yaml:"source,omitempty"` @@ -26,8 +29,13 @@ func init() { registerResource(MountResourceKey, &Mount{}) } -func (m *Mount) ID() string { return m.MountPoint } -func (m *Mount) SetID(id string) { m.MountPoint = id } +func (m *Mount) ID() string { + if m.MountPoint != "" && m.MountPoint != m.id { + return fmt.Sprintf("%s: %s", m.id, m.MountPoint) + } + return m.id +} +func (m *Mount) SetID(id string) { m.id = id } func (m *Mount) SetSkip() { m.Skip = true } func (m *Mount) TypeKey() string { return MountResourceKey } func (m *Mount) TypeName() string { return MountResourceName } @@ -35,10 +43,16 @@ func (m *Mount) TypeName() string { return MountResourceName } // FIXME: Can this be refactored? func (m *Mount) GetTitle() string { return m.Title } func (m *Mount) GetMeta() meta { return m.Meta } +func (m *Mount) GetMountPoint() string { + if m.MountPoint != "" { + return m.MountPoint + } + return m.id +} func (m *Mount) Validate(sys *system.System) []TestResult { skip := m.Skip - sysMount := sys.NewMount(m.MountPoint, sys, util.Config{}) + sysMount := sys.NewMount(m.GetMountPoint(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(m, "exists", m.Exists, sysMount.Exists, skip)) @@ -64,8 +78,8 @@ func NewMount(sysMount system.Mount, config util.Config) (*Mount, error) { mountPoint := sysMount.MountPoint() exists, _ := sysMount.Exists() m := &Mount{ - MountPoint: mountPoint, - Exists: exists, + id: mountPoint, + Exists: exists, } if !contains(config.IgnoreList, "opts") { if opts, err := sysMount.Opts(); err == nil { diff --git a/resource/package.go b/resource/package.go index 8678c8e8..aed11165 100644 --- a/resource/package.go +++ b/resource/package.go @@ -1,6 +1,8 @@ package resource import ( + "fmt" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) @@ -8,7 +10,8 @@ import ( type Package struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Name string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` Installed matcher `json:"installed" yaml:"installed"` Versions matcher `json:"versions,omitempty" yaml:"versions,omitempty"` Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` @@ -23,17 +26,28 @@ func init() { registerResource(PackageResourceKey, &Package{}) } -func (p *Package) ID() string { return p.Name } -func (p *Package) SetID(id string) { p.Name = id } +func (p *Package) ID() string { + if p.Name != "" && p.Name != p.id { + return fmt.Sprintf("%s: %s", p.id, p.Name) + } + return p.id +} +func (p *Package) SetID(id string) { p.id = id } func (p *Package) SetSkip() { p.Skip = true } func (p *Package) TypeKey() string { return PackageResourceKey } func (p *Package) TypeName() string { return PackageResourceName } func (p *Package) GetTitle() string { return p.Title } func (p *Package) GetMeta() meta { return p.Meta } +func (p *Package) GetName() string { + if p.Name != "" { + return p.Name + } + return p.id +} func (p *Package) Validate(sys *system.System) []TestResult { skip := p.Skip - sysPkg := sys.NewPackage(p.Name, sys, util.Config{}) + sysPkg := sys.NewPackage(p.GetName(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(p, "installed", p.Installed, sysPkg.Installed, skip)) @@ -50,7 +64,7 @@ func NewPackage(sysPackage system.Package, config util.Config) (*Package, error) name := sysPackage.Name() installed, _ := sysPackage.Installed() p := &Package{ - Name: name, + id: name, Installed: installed, } if !contains(config.IgnoreList, "versions") { diff --git a/resource/port.go b/resource/port.go index 7fd245cc..ce64666a 100644 --- a/resource/port.go +++ b/resource/port.go @@ -1,6 +1,8 @@ package resource import ( + "fmt" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) @@ -8,7 +10,8 @@ import ( type Port struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Port string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + Port string `json:"port,omitempty" yaml:"port,omitempty"` Listening matcher `json:"listening" yaml:"listening"` IP matcher `json:"ip,omitempty" yaml:"ip,omitempty"` Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` @@ -23,17 +26,28 @@ func init() { registerResource(PortResourceKey, &Port{}) } -func (p *Port) ID() string { return p.Port } -func (p *Port) SetID(id string) { p.Port = id } +func (p *Port) ID() string { + if p.Port != "" && p.Port != p.id { + return fmt.Sprintf("%s: %s", p.id, p.Port) + } + return p.id +} +func (p *Port) SetID(id string) { p.id = id } func (p *Port) SetSkip() { p.Skip = true } func (p *Port) TypeKey() string { return PortResourceKey } func (p *Port) TypeName() string { return PortResourceName } func (p *Port) GetTitle() string { return p.Title } func (p *Port) GetMeta() meta { return p.Meta } +func (p *Port) GetPort() string { + if p.Port != "" { + return p.Port + } + return p.id +} func (p *Port) Validate(sys *system.System) []TestResult { skip := p.Skip - sysPort := sys.NewPort(p.Port, sys, util.Config{}) + sysPort := sys.NewPort(p.GetPort(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(p, "listening", p.Listening, sysPort.Listening, skip)) @@ -50,7 +64,7 @@ func NewPort(sysPort system.Port, config util.Config) (*Port, error) { port := sysPort.Port() listening, _ := sysPort.Listening() p := &Port{ - Port: port, + id: port, Listening: listening, } if !contains(config.IgnoreList, "ip") { diff --git a/resource/process.go b/resource/process.go index 27cafded..0dc369c9 100644 --- a/resource/process.go +++ b/resource/process.go @@ -1,16 +1,19 @@ package resource import ( + "fmt" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) type Process struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Executable string `json:"-" yaml:"-"` - Running matcher `json:"running" yaml:"running"` - Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + id string `json:"-" yaml:"-"` + Comm string `json:"comm,omitempty" yaml:"comm,omitempty"` + Running matcher `json:"running" yaml:"running"` + Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` } const ( @@ -22,17 +25,28 @@ func init() { registerResource(ProcessResourceKey, &Process{}) } -func (p *Process) ID() string { return p.Executable } -func (p *Process) SetID(id string) { p.Executable = id } +func (p *Process) ID() string { + if p.Comm != "" && p.Comm != p.id { + return fmt.Sprintf("%s: %s", p.id, p.Comm) + } + return p.id +} +func (p *Process) SetID(id string) { p.id = id } func (p *Process) SetSkip() { p.Skip = true } func (p *Process) TypeKey() string { return ProcessResourceKey } func (p *Process) TypeName() string { return ProcessResourceName } func (p *Process) GetTitle() string { return p.Title } func (p *Process) GetMeta() meta { return p.Meta } +func (p *Process) GetComm() string { + if p.Comm != "" { + return p.Comm + } + return p.id +} func (p *Process) Validate(sys *system.System) []TestResult { skip := p.Skip - sysProcess := sys.NewProcess(p.Executable, sys, util.Config{}) + sysProcess := sys.NewProcess(p.GetComm(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(p, "running", p.Running, sysProcess.Running, skip)) @@ -46,7 +60,7 @@ func NewProcess(sysProcess system.Process, config util.Config) (*Process, error) return nil, err } return &Process{ - Executable: executable, - Running: running, + id: executable, + Running: running, }, nil } diff --git a/resource/resource.go b/resource/resource.go index 57477b2e..bff257e0 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -2,6 +2,7 @@ package resource import ( "fmt" + "os" "path/filepath" "strconv" "strings" @@ -57,7 +58,7 @@ func deprecateAtoI(depr any, desc string) any { if !ok { return depr } - fmt.Printf("DEPRECATION WARNING: %s should be an integer not a string\n", desc) + fmt.Fprintf(os.Stderr, "DEPRECATION WARNING: %s should be an integer not a string\n", desc) i, err := strconv.Atoi(s) if err != nil { panic(err) @@ -81,14 +82,17 @@ func shouldSkip(results []TestResult) bool { if len(results) < 1 { return false } - if results[0].Err != nil { - return true - } - if len(results[0].Found) < 1 { - return false - } - if results[0].Found[0] == "false" { + if results[0].Err != nil || results[0].Result != SUCCESS || results[0].MatcherResult.Actual == false { return true } return false } + +func isSet(i interface{}) bool { + switch v := i.(type) { + case []interface{}: + return len(v) > 0 + default: + return i != nil + } +} diff --git a/resource/service.go b/resource/service.go index 2163e6d6..84b33405 100644 --- a/resource/service.go +++ b/resource/service.go @@ -1,17 +1,21 @@ package resource import ( + "fmt" + "github.com/goss-org/goss/system" "github.com/goss-org/goss/util" ) type Service struct { - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Service string `json:"-" yaml:"-"` - Enabled matcher `json:"enabled" yaml:"enabled"` - Running matcher `json:"running" yaml:"running"` - Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + id string `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Enabled matcher `json:"enabled" yaml:"enabled"` + Running matcher `json:"running" yaml:"running"` + Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` + RunLevels matcher `json:"runlevels,omitempty" yaml:"runlevels,omitempty"` } const ( @@ -23,21 +27,39 @@ func init() { registerResource(ServiceResourceKey, &Service{}) } -func (s *Service) ID() string { return s.Service } -func (s *Service) SetID(id string) { s.Service = id } +func (s *Service) ID() string { + if s.Name != "" && s.Name != s.id { + return fmt.Sprintf("%s: %s", s.id, s.Name) + } + return s.id +} +func (s *Service) SetID(id string) { s.id = id } func (s *Service) SetSkip() { s.Skip = true } func (s *Service) TypeKey() string { return ServiceResourceKey } func (s *Service) TypeName() string { return ServiceResourceName } func (s *Service) GetTitle() string { return s.Title } func (s *Service) GetMeta() meta { return s.Meta } +func (s *Service) GetName() string { + if s.Name != "" { + return s.Name + } + return s.id +} func (s *Service) Validate(sys *system.System) []TestResult { skip := s.Skip - sysservice := sys.NewService(s.Service, sys, util.Config{}) + sysservice := sys.NewService(s.GetName(), sys, util.Config{}) var results []TestResult - results = append(results, ValidateValue(s, "enabled", s.Enabled, sysservice.Enabled, skip)) - results = append(results, ValidateValue(s, "running", s.Running, sysservice.Running, skip)) + if s.Enabled != nil { + results = append(results, ValidateValue(s, "enabled", s.Enabled, sysservice.Enabled, skip)) + } + if s.Running != nil { + results = append(results, ValidateValue(s, "running", s.Running, sysservice.Running, skip)) + } + if s.RunLevels != nil { + results = append(results, ValidateValue(s, "runlevels", s.RunLevels, sysservice.RunLevels, skip)) + } return results } @@ -52,7 +74,7 @@ func NewService(sysService system.Service, config util.Config) (*Service, error) return nil, err } return &Service{ - Service: service, + id: service, Enabled: enabled, Running: running, }, nil diff --git a/resource/user.go b/resource/user.go index c11c749a..d8ac634e 100644 --- a/resource/user.go +++ b/resource/user.go @@ -10,7 +10,8 @@ import ( type User struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` - Username string `json:"-" yaml:"-"` + id string `json:"-" yaml:"-"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` Exists matcher `json:"exists" yaml:"exists"` UID matcher `json:"uid,omitempty" yaml:"uid,omitempty"` GID matcher `json:"gid,omitempty" yaml:"gid,omitempty"` @@ -29,17 +30,28 @@ func init() { registerResource(UserResourceKey, &User{}) } -func (u *User) ID() string { return u.Username } -func (u *User) SetID(id string) { u.Username = id } +func (u *User) ID() string { + if u.Username != "" && u.Username != u.id { + return fmt.Sprintf("%s: %s", u.id, u.Username) + } + return u.id +} +func (u *User) SetID(id string) { u.id = id } func (u *User) SetSkip() { u.Skip = true } func (u *User) TypeKey() string { return UserResourceKey } func (u *User) TypeName() string { return UserResourceName } func (u *User) GetTitle() string { return u.Title } func (u *User) GetMeta() meta { return u.Meta } +func (u *User) GetUsername() string { + if u.Username != "" { + return u.Username + } + return u.id +} func (u *User) Validate(sys *system.System) []TestResult { skip := u.Skip - sysuser := sys.NewUser(u.Username, sys, util.Config{}) + sysuser := sys.NewUser(u.GetUsername(), sys, util.Config{}) var results []TestResult results = append(results, ValidateValue(u, "exists", u.Exists, sysuser.Exists, skip)) @@ -70,8 +82,8 @@ func NewUser(sysUser system.User, config util.Config) (*User, error) { username := sysUser.Username() exists, _ := sysUser.Exists() u := &User{ - Username: username, - Exists: exists, + id: username, + Exists: exists, } if !contains(config.IgnoreList, "uid") { if uid, err := sysUser.UID(); err == nil { diff --git a/resource/validate.go b/resource/validate.go index 55cf8c1e..95525eaf 100644 --- a/resource/validate.go +++ b/resource/validate.go @@ -1,16 +1,13 @@ package resource import ( - "bufio" - "encoding/json" "fmt" "io" "reflect" - "regexp" "strings" "time" - "github.com/onsi/gomega/types" + "github.com/goss-org/goss/matchers" ) const ( @@ -60,20 +57,24 @@ func toValidateError(err error) *ValidateError { } type TestResult struct { - Successful bool `json:"successful" yaml:"successful"` - Skipped bool `json:"skipped" yaml:"skipped"` - ResourceId string `json:"resource-id" yaml:"resource-id"` - ResourceType string `json:"resource-type" yaml:"resource-type"` - Title string `json:"title" yaml:"title"` - Meta meta `json:"meta" yaml:"meta"` - TestType int `json:"test-type" yaml:"test-type"` - Result int `json:"result" yaml:"result"` - Property string `json:"property" yaml:"property"` - Err *ValidateError `json:"err" yaml:"err"` - Expected []string `json:"expected" yaml:"expected"` - Found []string `json:"found" yaml:"found"` - Human string `json:"human" yaml:"human"` - Duration time.Duration `json:"duration" yaml:"duration"` + Successful bool `json:"successful" yaml:"successful"` + Skipped bool `json:"skipped" yaml:"skipped"` + // Resource data + ResourceId string `json:"resource-id" yaml:"resource-id"` + ResourceType string `json:"resource-type" yaml:"resource-type"` + Property string `json:"property" yaml:"property"` + + // User added info + Title string `json:"title" yaml:"title"` + Meta meta `json:"meta" yaml:"meta"` + + // Result + Result int `json:"result" yaml:"result"` + Err *ValidateError `json:"err" yaml:"err"` + MatcherResult matchers.MatcherResult `json:"matcher-result" yaml:"matcher-result"` + StartTime time.Time `json:"start-time" yaml:"start-time"` + EndTime time.Time `json:"end-time" yaml:"end-time"` + Duration time.Duration `json:"duration" yaml:"duration"` } // ToOutcome converts the enum to a human-friendly string. @@ -90,22 +91,46 @@ func (tr TestResult) ToOutcome() string { } } -func skipResult(typeS string, testType int, id string, title string, meta meta, property string, startTime time.Time) TestResult { +func (t TestResult) SortKey() string { + return fmt.Sprintf("%s:%s", t.ResourceType, t.ResourceId) +} + +func skipResult(typeS string, id string, title string, meta meta, property string, startTime time.Time) TestResult { + endTime := time.Now() return TestResult{ - Successful: true, Result: SKIP, Skipped: true, ResourceType: typeS, - TestType: testType, ResourceId: id, Title: title, Meta: meta, Property: property, - Duration: startTime.Sub(startTime), + StartTime: startTime, + EndTime: endTime, + Duration: endTime.Sub(startTime), } } func ValidateValue(res ResourceRead, property string, expectedValue any, actual any, skip bool) TestResult { + if f, ok := actual.(func() (io.Reader, error)); ok { + if _, ok := expectedValue.([]any); !ok { + actual = func() (string, error) { + v, err := f() + if err != nil { + return "", err + } + i, err := matchers.ReaderToString{}.Transform(v) + if err != nil { + return "", err + } + return i.(string), nil + } + } + } + return ValidateGomegaValue(res, property, expectedValue, actual, skip) +} + +func ValidateGomegaValue(res ResourceRead, property string, expectedValue any, actual any, skip bool) TestResult { id := res.ID() title := res.GetTitle() meta := res.GetMeta() @@ -115,7 +140,6 @@ func ValidateValue(res ResourceRead, property string, expectedValue any, actual if skip { return skipResult( typeS, - Values, id, title, meta, @@ -125,6 +149,7 @@ func ValidateValue(res ResourceRead, property string, expectedValue any, actual } var foundValue any + var gomegaMatcher matchers.GossMatcher var err error switch f := actual.(type) { case func() (bool, error): @@ -137,295 +162,62 @@ func ValidateValue(res ResourceRead, property string, expectedValue any, actual foundValue, err = f() case func() (any, error): foundValue, err = f() + case func() (io.Reader, error): + foundValue, err = f() + gomegaMatcher = matchers.HavePatterns(expectedValue) default: err = fmt.Errorf("Unknown method signature: %t", f) } + foundValue = sanitizeExpectedValue(foundValue) expectedValue = sanitizeExpectedValue(expectedValue) - var gomegaMatcher types.GomegaMatcher var success bool - if err == nil { + if gomegaMatcher == nil && err == nil { gomegaMatcher, err = matcherToGomegaMatcher(expectedValue) } - if err == nil { - success, err = gomegaMatcher.Match(foundValue) - } - if err != nil { - return TestResult{ - Successful: false, - Result: FAIL, - ResourceType: typeS, - TestType: Values, - ResourceId: id, - Title: title, - Meta: meta, - Property: property, - Err: toValidateError(err), - Duration: time.Now().Sub(startTime), - } - } - - var failMessage string - var result int - if !success { - failMessage = gomegaMatcher.FailureMessage(foundValue) - result = FAIL - } - - expected, _ := json.Marshal(expectedValue) - found, _ := json.Marshal(foundValue) - - return TestResult{ - Successful: success, - Result: result, - ResourceType: typeS, - TestType: Value, - ResourceId: id, - Title: title, - Meta: meta, - Property: property, - Expected: []string{string(expected)}, - Found: []string{string(found)}, - Human: failMessage, - Duration: time.Now().Sub(startTime), - } -} - -type patternMatcher interface { - Match(string) bool - Pattern() string - Inverse() bool -} - -type stringPattern struct { - pattern string - cleanPattern string - inverse bool -} - -func newStringPattern(str string) *stringPattern { - var inverse bool - if strings.HasPrefix(str, "!") { - inverse = true - } - cleanPattern := strings.TrimLeft(str, "\\/!") - return &stringPattern{ - pattern: str, - cleanPattern: cleanPattern, - inverse: inverse, - } -} - -func (s *stringPattern) Match(str string) bool { - return strings.Contains(str, s.cleanPattern) -} - -func (s *stringPattern) Pattern() string { return s.pattern } -func (s *stringPattern) Inverse() bool { return s.inverse } - -type regexPattern struct { - pattern string - re *regexp.Regexp - inverse bool -} - -func newRegexPattern(str string) (*regexPattern, error) { - var inverse bool - cleanStr := str - if strings.HasPrefix(str, "!") { - inverse = true - cleanStr = cleanStr[1:] - } - trimLeft := []rune{'\\', '/'} - for _, r := range trimLeft { - if rune(cleanStr[0]) == r { - cleanStr = cleanStr[1:] - break - } - } - trimRight := []rune{'/'} - for _, r := range trimRight { - if rune(cleanStr[len(cleanStr)-1]) == r { - cleanStr = cleanStr[:len(cleanStr)-1] - break - } - } - - re, err := regexp.Compile(cleanStr) - - return ®exPattern{ - pattern: str, - re: re, - inverse: inverse, - }, err - -} - -func (re *regexPattern) Match(str string) bool { - return re.re.MatchString(str) -} - -func (re *regexPattern) Pattern() string { return re.pattern } -func (re *regexPattern) Inverse() bool { return re.inverse } - -func sliceToPatterns(slice []string) ([]patternMatcher, error) { - var patterns []patternMatcher - for _, s := range slice { - if (strings.HasPrefix(s, "/") || strings.HasPrefix(s, "!/")) && strings.HasSuffix(s, "/") { - pat, err := newRegexPattern(s) - if err != nil { - return nil, err - } - patterns = append(patterns, pat) - } else { - patterns = append(patterns, newStringPattern(s)) - } - } - return patterns, nil -} - -func patternsToSlice(patterns []patternMatcher) []string { - var slice []string - for _, p := range patterns { - slice = append(slice, p.Pattern()) - } - return slice -} - -func ValidateContains(res ResourceRead, property string, expectedValues []string, method func() (io.Reader, error), skip bool) TestResult { - id := res.ID() - title := res.GetTitle() - meta := res.GetMeta() - typ := reflect.TypeOf(res) - typeS := strings.Split(typ.String(), ".")[1] - startTime := time.Now() - if skip { - return skipResult( - typeS, - Values, - id, - title, - meta, - property, - startTime, - ) - } - var err error - var fh io.Reader - var notfound []patternMatcher - notfound, err = sliceToPatterns(expectedValues) - // short circuit - if len(notfound) == 0 && err == nil { - return TestResult{ - Successful: true, - Result: SUCCESS, - ResourceType: typeS, - TestType: Contains, - ResourceId: id, - Title: title, - Meta: meta, - Property: property, - Expected: expectedValues, - Duration: time.Now().Sub(startTime), - } - } - if err == nil { - fh, err = method() - } if err != nil { + endTime := time.Now() return TestResult{ - Successful: false, Result: FAIL, ResourceType: typeS, - TestType: Contains, ResourceId: id, Title: title, Meta: meta, Property: property, Err: toValidateError(err), - Duration: time.Now().Sub(startTime), + StartTime: startTime, + EndTime: endTime, + Duration: endTime.Sub(startTime), } } - defer func() { - // Do we need to close the stream? - if rc, ok := fh.(io.ReadCloser); ok { - rc.Close() - } - }() - - scanner := bufio.NewScanner(fh) - scanner.Buffer(nil, maxScanTokenSize) - var found []patternMatcher - for scanner.Scan() { - line := scanner.Text() + success, err = gomegaMatcher.Match(foundValue) - i := 0 - for _, pat := range notfound { - if pat.Match(line) { - // Found it, but wasn't supposed to, don't mark it as found, but remove it from search - if !pat.Inverse() { - found = append(found, pat) - } - continue - } - notfound[i] = pat - i++ - } - notfound = notfound[:i] - if len(notfound) == 0 { - break - } - } - if err := scanner.Err(); err != nil { - return TestResult{ - Successful: false, - Result: FAIL, - ResourceType: typeS, - TestType: Contains, - ResourceId: id, - Title: title, - Meta: meta, - Property: property, - Err: toValidateError(err), - Duration: time.Now().Sub(startTime), - } - } - - for _, pat := range notfound { - // Didn't find it, but we didn't want to.. so we mark it as found - // Empty pattern should match even if input to scanner is empty - if pat.Inverse() || pat.Pattern() == "" { - found = append(found, pat) + var matcherResult matchers.MatcherResult + result := SUCCESS + if success { + matcherResult = matchers.MatcherResult{ + Actual: foundValue, + Message: "matches expectation", + Expected: expectedValue, } + } else { + matcherResult = gomegaMatcher.FailureResult(foundValue) + result = FAIL } - if len(expectedValues) != len(found) { - return TestResult{ - Successful: false, - Result: FAIL, - ResourceType: typeS, - TestType: Contains, - ResourceId: id, - Title: title, - Meta: meta, - Property: property, - Expected: expectedValues, - Found: patternsToSlice(found), - Duration: time.Now().Sub(startTime), - } - } + endTime := time.Now() return TestResult{ - Successful: true, - Result: SUCCESS, - ResourceType: typeS, - TestType: Contains, - ResourceId: id, - Title: title, - Meta: meta, - Property: property, - Expected: expectedValues, - Found: patternsToSlice(found), - Duration: time.Now().Sub(startTime), + Result: result, + ResourceType: typeS, + ResourceId: id, + Title: title, + Meta: meta, + Property: property, + MatcherResult: matcherResult, + Err: toValidateError(err), + StartTime: startTime, + EndTime: endTime, + Duration: endTime.Sub(startTime), } } diff --git a/resource/validate_test.go b/resource/validate_test.go index 998f4431..55a45020 100644 --- a/resource/validate_test.go +++ b/resource/validate_test.go @@ -21,13 +21,13 @@ func (f *FakeResource) GetMeta() meta { return meta{"foo": "bar"} } var stringTests = []struct { in, in2 any - want bool + want int }{ - {"", "", true}, - {"foo", "foo", true}, - {"foo", "bar", false}, - {"foo", "", false}, - {true, true, true}, + {"", "", SUCCESS}, + {"foo", "foo", SUCCESS}, + {"foo", "bar", FAIL}, + {"foo", "", FAIL}, + {true, true, SUCCESS}, } func TestValidateValue(t *testing.T) { @@ -36,8 +36,8 @@ func TestValidateValue(t *testing.T) { return c.in2, nil } got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false) - if got.Successful != c.want { - t.Errorf("%+v: got %v, want %v", c, got.Successful, c.want) + if got.Result != c.want { + t.Errorf("%+v: got %v, want %v", c, got.Result, c.want) } } } @@ -48,8 +48,8 @@ func TestValidateValueErr(t *testing.T) { return c.in2, fmt.Errorf("some err") } got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false) - if got.Successful != false { - t.Errorf("%+v: got %v, want %v", c, got.Successful, false) + if got.Result != FAIL { + t.Errorf("%+v: got %v, want %v", c, got.Result, FAIL) } } } @@ -76,19 +76,19 @@ func BenchmarkValidateValue(b *testing.B) { } var containsTests = []struct { - in []string + in []interface{} in2 string - want bool + want int }{ - {[]string{""}, "", true}, - {[]string{"foo"}, "foo\nbar", true}, - {[]string{"!foo"}, "foo\nbar", false}, - {[]string{"!moo"}, "foo\nbar", true}, - {[]string{"/fo.*/"}, "foo\nbar", true}, - {[]string{"!/fo.*/"}, "foo\nbar", false}, - {[]string{"!/mo.*/"}, "foo\nbar", true}, - {[]string{"foo"}, "", false}, - {[]string{`/\s/tmp\b/`}, "test /tmp bar", true}, + {[]interface{}{""}, "", SUCCESS}, + {[]interface{}{"foo"}, "foo\nbar", SUCCESS}, + {[]interface{}{"!foo"}, "foo\nbar", FAIL}, + {[]interface{}{"!moo"}, "foo\nbar", SUCCESS}, + {[]interface{}{"/fo.*/"}, "foo\nbar", SUCCESS}, + {[]interface{}{"!/fo.*/"}, "foo\nbar", FAIL}, + {[]interface{}{"!/mo.*/"}, "foo\nbar", SUCCESS}, + {[]interface{}{"foo"}, "", FAIL}, + {[]interface{}{`/\s/tmp\b/`}, "test /tmp bar", SUCCESS}, } func TestValidateContains(t *testing.T) { @@ -97,9 +97,9 @@ func TestValidateContains(t *testing.T) { reader := strings.NewReader(c.in2) return reader, nil } - got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, false) - if got.Successful != c.want { - t.Errorf("%+v: got %v, want %v", c, got.Successful, c.want) + got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false) + if got.Result != c.want { + t.Errorf("%+v: got %v, want %v", c, got.Result, c.want) } } } @@ -110,9 +110,9 @@ func TestValidateContainsErr(t *testing.T) { reader := strings.NewReader(c.in2) return reader, fmt.Errorf("some err") } - got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, false) - if got.Successful != false { - t.Errorf("%+v: got %v, want %v", c, got.Successful, false) + got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, false) + if got.Result != FAIL { + t.Errorf("%+v: got %v, want %v", c, got.Result, FAIL) } } } @@ -122,7 +122,7 @@ func TestValidateContainsBadRegexErr(t *testing.T) { reader := strings.NewReader("dummy") return reader, nil } - got := ValidateContains(&FakeResource{""}, "", []string{"/*\\.* @@.*/"}, inFunc, false) + got := ValidateValue(&FakeResource{""}, "", []interface{}{"/*\\.* @@.*/"}, inFunc, false) if got.Err == nil { t.Errorf("Expected bad regex to raise error, got nil") } @@ -134,7 +134,7 @@ func TestValidateContainsSkip(t *testing.T) { reader := strings.NewReader(c.in2) return reader, nil } - got := ValidateContains(&FakeResource{""}, "", c.in, inFunc, true) + got := ValidateValue(&FakeResource{""}, "", c.in, inFunc, true) if got.Result != SKIP { t.Errorf("%+v: got %v, want %v", c, got.Result, SKIP) } @@ -145,7 +145,7 @@ func TestResultMarshaling(t *testing.T) { inFunc := func() (io.Reader, error) { return nil, fmt.Errorf("dummy error") } - res := ValidateContains(&FakeResource{}, "", []string{"x"}, inFunc, false) + res := ValidateValue(&FakeResource{}, "", []string{"x"}, inFunc, false) if res.Err == nil { t.Fatalf("Expected to receive an error") } diff --git a/serve.go b/serve.go index ec4b1a5b..3adb3427 100644 --- a/serve.go +++ b/serve.go @@ -96,36 +96,28 @@ func (h healthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (h healthHandler) processAndEnsureCached(negotiatedContentType string, outputer outputs.Outputer) res { - cacheKey := fmt.Sprintf("res:%s", negotiatedContentType) + var tra [][]resource.TestResult + cacheKey := "res" tmp, found := h.cache.Get(cacheKey) if found { - log.Printf("[TRACE] Returning cached[%s] (1).", cacheKey) - return tmp.(res) - } - - h.gossMu.Lock() - defer h.gossMu.Unlock() - tmp, found = h.cache.Get(cacheKey) - if found { - log.Printf("[TRACE] Returning cached[%s] (2).", cacheKey) - return tmp.(res) + log.Printf("[TRACE] Returning cached[%s].", cacheKey) + tra = tmp.([][]resource.TestResult) + } else { + log.Printf("Stale cache[%s], running tests", cacheKey) + h.sys = system.New(h.c.PackageManager) + tra = h.validate() + h.cache.SetDefault(cacheKey, tra) } - - log.Printf("[TRACE] Stale cache[%s], running tests", cacheKey) - resp := h.runValidate(outputer) - h.cache.SetDefault(cacheKey, resp) - return resp + trc := testResultArrayToChan(tra) + return h.output(trc, outputer) } -func (h healthHandler) runValidate(outputer outputs.Outputer) res { - h.sys = system.New(h.c.PackageManager) - iStartTime := time.Now() - out := validate(h.sys, h.gossConfig, h.c.DisabledResourceTypes, h.maxConcurrent) +func (h healthHandler) output(trc <-chan []resource.TestResult, outputer outputs.Outputer) res { var b bytes.Buffer outputConfig := util.OutputConfig{ FormatOptions: h.c.FormatOptions, } - exitCode := outputer.Output(&b, out, iStartTime, outputConfig) + exitCode := outputer.Output(&b, trc, outputConfig) resp := res{ body: b, } @@ -136,6 +128,28 @@ func (h healthHandler) runValidate(outputer outputs.Outputer) res { } return resp } +func (h healthHandler) validate() [][]resource.TestResult { + h.sys = system.New(h.c.PackageManager) + res := make([][]resource.TestResult, 0) + tr := validate(h.sys, h.gossConfig, h.c.DisabledResourceTypes, h.maxConcurrent) + for i := range tr { + res = append(res, i) + } + return res +} + +func testResultArrayToChan(tra [][]resource.TestResult) <-chan []resource.TestResult { + c := make(chan []resource.TestResult) + go func(c chan []resource.TestResult) { + defer close(c) + + for _, i := range tra { + c <- i + } + }(c) + + return c +} const ( // https://en.wikipedia.org/wiki/Media_type @@ -174,12 +188,3 @@ func (h healthHandler) responseContentType(outputName string) string { } return fmt.Sprintf("%s%s", mediaTypePrefix, outputName) } - -func (h healthHandler) renderBody(results <-chan []resource.TestResult, outputer outputs.Outputer) (int, bytes.Buffer) { - outputConfig := util.OutputConfig{ - FormatOptions: h.c.FormatOptions, - } - var b bytes.Buffer - exitCode := outputer.Output(&b, results, time.Now(), outputConfig) - return exitCode, b -} diff --git a/serve_test.go b/serve_test.go index a6138f93..b29fe3ba 100644 --- a/serve_test.go +++ b/serve_test.go @@ -268,14 +268,14 @@ func TestServeCacheNegotiatingContent(t *testing.T) { logOutput.Reset() }) - t.Run("immediately re-request but different accept header, cache should be cold", func(t *testing.T) { + t.Run("immediately re-request but different accept header, cache should be warm", func(t *testing.T) { req := makeRequest(t, config, map[string][]string{ "accept": {"application/vnd.goss-rspecish"}, }) handler.ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Result().StatusCode) - assert.Contains(t, logOutput.String(), "Stale cache") + assert.NotContains(t, logOutput.String(), "Stale cache") t.Log(logOutput.String()) logOutput.Reset() }) diff --git a/store.go b/store.go index abd8f10b..0f4e560b 100644 --- a/store.go +++ b/store.go @@ -76,12 +76,12 @@ func (t *TmplVars) Env() map[string]string { func loadVars(varsFile string, varsInline string) (map[string]any, error) { vars, err := varsFromFile(varsFile) if err != nil { - return nil, fmt.Errorf("Error: loading vars file '%s'\n%w", varsFile, err) + return nil, fmt.Errorf("loading vars file '%s'\n%w", varsFile, err) } varsExtra, err := varsFromString(varsInline) if err != nil { - return nil, fmt.Errorf("Error: loading inline vars\n%w", err) + return nil, fmt.Errorf("loading inline vars\n%w", err) } for k, v := range varsExtra { @@ -142,7 +142,7 @@ func ReadJSONData(data []byte, detectFormat bool) (GossConfig, error) { } format := outStoreFormat - if detectFormat == true { + if detectFormat { format, err = getStoreFormatFromData(data) if err != nil { return GossConfig{}, err @@ -314,10 +314,5 @@ func marshalYAML(gossConfig any) ([]byte, error) { } func unmarshalYAML(data []byte, v any) error { - err := yaml.Unmarshal(data, v) - if err != nil { - return fmt.Errorf("could not unmarshal %q as YAML data: %s", string(data), err) - } - - return nil + return yaml.Unmarshal(data, v) } diff --git a/system/file.go b/system/file.go index bb2a85ba..728194f9 100644 --- a/system/file.go +++ b/system/file.go @@ -19,7 +19,7 @@ import ( type File interface { Path() string Exists() (bool, error) - Contains() (io.Reader, error) + Contents() (io.Reader, error) Mode() (string, error) Size() (int, error) Filetype() (string, error) @@ -83,7 +83,7 @@ func (f *DefFile) Exists() (bool, error) { return true, err } -func (f *DefFile) Contains() (io.Reader, error) { +func (f *DefFile) Contents() (io.Reader, error) { if err := f.setup(); err != nil { return nil, err } diff --git a/system/http.go b/system/http.go index d821a1ee..51c669dd 100644 --- a/system/http.go +++ b/system/http.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "os" + "sort" "strings" "time" @@ -72,6 +73,7 @@ func HeaderToArray(header http.Header) (res []string) { res = append(res, fmt.Sprintf("%s: %s", name, value)) } } + sort.Strings(res) return } diff --git a/system/mount.go b/system/mount.go index bbdfe58b..557dcbbc 100644 --- a/system/mount.go +++ b/system/mount.go @@ -6,6 +6,7 @@ import ( "github.com/goss-org/goss/util" "github.com/moby/sys/mountinfo" + "github.com/samber/lo" ) type Mount interface { @@ -77,8 +78,9 @@ func (m *DefMount) Opts() ([]string, error) { if err := m.setup(); err != nil { return nil, err } + allOpts := splitMountInfo(strings.Join([]string{m.mountInfo.Options, m.mountInfo.VFSOptions}, ",")) - return strings.Split(m.mountInfo.Options, ","), nil + return lo.Uniq(allOpts), nil } func (m *DefMount) Source() (string, error) { @@ -106,20 +108,22 @@ func (m *DefMount) Usage() (int, error) { } func getMount(mountpoint string) (*mountinfo.Info, error) { - entries, err := mountinfo.GetMounts(func(e *mountinfo.Info) (skip bool, stop bool) { - if e.Mountpoint == mountpoint { - return false, true - } - - return true, false - }) + entries, err := mountinfo.GetMounts(mountinfo.SingleEntryFilter(mountpoint)) if err != nil { return nil, err } - if len(entries) == 0 { return nil, fmt.Errorf("Mountpoint not found") } - return entries[0], nil } + +func splitMountInfo(s string) []string { + quoted := false + return strings.FieldsFunc(s, func(r rune) bool { + if r == '"' { + quoted = !quoted + } + return !quoted && r == ',' + }) +} diff --git a/system/mount_test.go b/system/mount_test.go new file mode 100644 index 00000000..b9835c55 --- /dev/null +++ b/system/mount_test.go @@ -0,0 +1,21 @@ +package system + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestSplitMountInfo(t *testing.T) { + in := "rw,context=\"system_u:object_r:container_file_t:s0:c174,c741\",size=65536k,mode=755" + want := []string{ + "rw", + "context=\"system_u:object_r:container_file_t:s0:c174,c741\"", + "size=65536k", + "mode=755", + } + + got := splitMountInfo(in) + + assert.DeepEqual(t, got, want) +} diff --git a/system/package_rpm.go b/system/package_rpm.go index 4303573e..9d6519f3 100644 --- a/system/package_rpm.go +++ b/system/package_rpm.go @@ -23,7 +23,7 @@ func (p *RpmPackage) setup() { return } p.loaded = true - cmd := util.NewCommand("rpm", "-q", "--nosignature", "--nohdrchk", "--nodigest", "--qf", "%{VERSION}\n", p.name) + cmd := util.NewCommand("rpm", "-q", "--nosignature", "--nohdrchk", "--nodigest", "--qf", "%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\n", p.name) if err := cmd.Run(); err != nil { return } diff --git a/system/service.go b/system/service.go index 79cbdcf3..df2a43dc 100644 --- a/system/service.go +++ b/system/service.go @@ -7,6 +7,7 @@ type Service interface { Exists() (bool, error) Enabled() (bool, error) Running() (bool, error) + RunLevels() ([]string, error) } func invalidService(s string) bool { diff --git a/system/service_init.go b/system/service_init.go index 56f8a14e..590e43f2 100644 --- a/system/service_init.go +++ b/system/service_init.go @@ -4,13 +4,15 @@ import ( "fmt" "os" "path/filepath" + "regexp" "github.com/goss-org/goss/util" ) type ServiceInit struct { - service string - alpine bool + service string + alpine bool + runlevel string } func NewServiceInit(service string, system *System, config util.Config) Service { @@ -18,7 +20,11 @@ func NewServiceInit(service string, system *System, config util.Config) Service } func NewAlpineServiceInit(service string, system *System, config util.Config) Service { - return &ServiceInit{service: service, alpine: true} + runlevel := config.RunLevel + if runlevel == "" { + runlevel = "sysinit" + } + return &ServiceInit{service: service, alpine: true, runlevel: runlevel} } func (s *ServiceInit) Service() string { @@ -39,10 +45,24 @@ func (s *ServiceInit) Enabled() (bool, error) { if invalidService(s.service) { return false, nil } + var runLevels []string + var err error + if s.alpine { + runLevels, err = alpineServiceRunLevels(s.service) + } else { + runLevels, err = initServiceRunLevels(s.service) + } + return len(runLevels) != 0, err +} + +func (s *ServiceInit) RunLevels() ([]string, error) { + if invalidService(s.service) { + return nil, nil + } if s.alpine { - return alpineInitServiceEnabled(s.service, "sysinit") + return alpineServiceRunLevels(s.service) } else { - return initServiceEnabled(s.service, 3) + return initServiceRunLevels(s.service) } } @@ -58,18 +78,34 @@ func (s *ServiceInit) Running() (bool, error) { return false, nil } -func initServiceEnabled(service string, level int) (bool, error) { - matches, err := filepath.Glob(fmt.Sprintf("/etc/rc%d.d/S[0-9][0-9]%s", level, service)) - if err == nil && matches != nil { - return true, nil +func initServiceRunLevels(service string) ([]string, error) { + var runLevels []string + matches, err := filepath.Glob(fmt.Sprintf("/etc/rc*.d/S[0-9][0-9]%s", service)) + if err != nil { + return nil, err } - return false, err + re := regexp.MustCompile("/etc/rc([0-9]+).d/") + for _, m := range matches { + matches := re.FindStringSubmatch(m) + if matches != nil { + runLevels = append(runLevels, matches[1]) + } + } + return runLevels, nil } -func alpineInitServiceEnabled(service string, level string) (bool, error) { - matches, err := filepath.Glob(fmt.Sprintf("/etc/runlevels/%s/%s", level, service)) - if err == nil && matches != nil { - return true, nil +func alpineServiceRunLevels(service string) ([]string, error) { + var runLevels []string + matches, err := filepath.Glob(fmt.Sprintf("/etc/runlevels/*/%s", service)) + if err != nil { + return nil, err + } + re := regexp.MustCompile("/etc/runlevels/([^/]+)") + for _, m := range matches { + matches := re.FindStringSubmatch(m) + if matches != nil { + runLevels = append(runLevels, matches[1]) + } } - return false, err + return runLevels, nil } diff --git a/system/service_systemd.go b/system/service_systemd.go index 6e48322e..4a950500 100644 --- a/system/service_systemd.go +++ b/system/service_systemd.go @@ -85,3 +85,7 @@ func (s *ServiceSystemd) Running() (bool, error) { } return false, nil } + +func (s *ServiceSystemd) RunLevels() ([]string, error) { + return nil, nil +} diff --git a/system/service_upstart.go b/system/service_upstart.go index ab60590a..e5a9dcb2 100644 --- a/system/service_upstart.go +++ b/system/service_upstart.go @@ -77,3 +77,7 @@ func (s *ServiceUpstart) Running() (bool, error) { } return false, nil } +func (s *ServiceUpstart) RunLevels() ([]string, error) { + sysv := &ServiceInit{service: s.service} + return sysv.RunLevels() +} diff --git a/system/system.go b/system/system.go index 7ab5a0eb..dfef3985 100644 --- a/system/system.go +++ b/system/system.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/goss-org/GOnetstat" + // This needs a better name "github.com/goss-org/go-ps" util2 "github.com/goss-org/goss/util" diff --git a/testdata/matching_basic.yaml b/testdata/matching_basic.yaml new file mode 100644 index 00000000..edd9ee67 --- /dev/null +++ b/testdata/matching_basic.yaml @@ -0,0 +1,114 @@ +matching: + # Basic matchers + basic_string: + content: 'this is a test' + matches: 'this is a test' + + basic_string_regexp: + content: 'this is a test' + matches: + match-regexp: '^this' + + basic_string_skip: + skip: true + content: 'this is a test' + matches: 'this is a test' + + basic_semver: + content: '1.2.3' + matches: + semver-constraint: '>=1.2.0' + + basic_int: + content: 42 + matches: 42 + + basic_array: + content: + - 'group1' + - 'group2' + - 'group3' + matches: + - 'group1' + - 'group2' + + basic_array_matchers: + content: [foo, bar, moo] + matches: + and: + - contain-elements: [foo, bar] + - [foo, bar] # same as above + - equal: [foo, bar, moo] # order matters, exact match + - consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers + - contain-element: + have-prefix: b + - contain-element: + have-suffix: r + + basic_reader: + as-reader: true + content: | + foo bar + moo cow + matches: + - 'foo' + - '/^m.*w$/' + - '!ftw' + - '!/^ERROR:/' + + # Negated + negated_basic_string: + content: 'this is a test' + matches: + not: 'this is a failing test' + + negated_basic_string_regexp: + content: 'this is a test' + matches: + not: + match-regexp: '^foo' + + negated_basic_int: + content: 42 + matches: + not: 43 + + negated_basic_array: + content: + - 'group1' + - 'group2' + - 'group3' + matches: + not: + - 'group1' + - 'group2' + - 'group2' + - 'group4' + + negated_basic_array_matchers: + content: [foo, bar, moo] + matches: + and: + - not: + contain-elements: [fox, box] + - not: [fox, bax] # same as above + - not: + equal: [fox, bax, mox] # order matters, exact match + - not: + consist-of: [have-suffix: x, have-prefix: t, box] # order doesn't matter, can use matchers + - not: + contain-element: + have-prefix: x + + negated_basic_reader: + as-reader: true + content: | + foo bar + moo cow + matches: + not: + contain-elements: + - 'fox' + - '/^t.*w$/' + - '!foo' + - '!/^foo/' diff --git a/testdata/matching_basic_failing.yaml b/testdata/matching_basic_failing.yaml new file mode 100644 index 00000000..51a6a427 --- /dev/null +++ b/testdata/matching_basic_failing.yaml @@ -0,0 +1,181 @@ +matching: + # Basic matchers + basic_string: + content: 'this is a test' + matches: 'this is a failing test' + + basic_string_have_prefix: + content: 'foo' + matches: + have-prefix: 'g' + + basic_string_have_suffix: + content: 'foo' + matches: + have-suffix: 'x' + + basic_string_contain_substring: + content: 'foo' + matches: + contain-substring: 'x' + + basic_string_regexp: + content: 'this is a test' + matches: + match-regexp: '^foo' + + basic_semver: + content: '1.2.3' + matches: + or: + - semver-constraint: '>=9.9.0' + + basic_len: + content: "123" + matches: + or: + - have-len: 2 + + basic_int: + content: 42 + matches: 43 + + basic_array: + content: + - 'group1' + - 'group2' + - 'group3' + matches: + - 'group1' + - 'group2' + - 'group2' + - 'group4' + + basic_array_matchers: + content: [foo, bar, moo] + matches: + or: + - contain-elements: [fox, box] + - [fox, bax] # same as above + - equal: [fox, bax, mox] # order matters, exact match + - consist-of: [fox, have-prefix: t, box] # order doesn't matter, can use matchers + - contain-element: + have-prefix: x + - contain-element: + have-suffix: x + + basic_array_consists_of: + content: [foo, bar, moo] + matches: + consist-of: [fox, have-prefix: t, box] # order doesn't matter, can use matchers + + basic_reader: + as-reader: true + content: | + foo bar + moo cow + matches: + - 'fox' + - '/^t.*w$/' + - '!foo' + - '!/^foo/' + + # Negated + negated_basic_string: + content: 'this is a test' + matches: + not: 'this is a test' + + negatedbasic_string_regexp: + content: 'this is a test' + matches: + not: + match-regexp: '^this' + + negatedbasic_string_have_prefix: + content: 'foo' + matches: + not: + have-prefix: 'f' + + negatedbasic_string_have_suffix: + content: 'foo' + matches: + not: + have-suffix: 'o' + + negatedbasic_string_contain_substring: + content: 'foo' + matches: + not: + contain-substring: 'oo' + + negatedbasic_len: + content: '123' + matches: + not: + have-len: 3 + + negated_basic_int: + content: 42 + matches: + not: 42 + + negated_and: + content: 42 + matches: + not: + and: + - 42 + - 42 + + negated_basic_array: + content: + - 'group1' + - 'group2' + - 'group3' + matches: + not: + - 'group1' + - 'group2' + - 'group3' + + negated_basic_array_matchers: + content: [foo, bar, moo] + matches: + or: + - not: + contain-elements: [foo, bar] + - not: + [foo, bar] # same as above + - not: + equal: [foo, bar, moo] # order matters, exact match + - not: + consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers + - not: + contain-element: + have-prefix: b + + negated_basic_array_contain_element: + content: [foo, bar, moo] + matches: + not: + contain-element: foo + + negated_basic_array_consists_of: + content: [foo, bar, moo] + matches: + not: + consist-of: [foo, have-prefix: m, bar] # order doesn't matter, can use matchers + + negated_basic_reader: + as-reader: true + content: | + foo bar + moo cow + matches: + not: + - 'foo' + - '/^m.*w$/' + - '!ftw' + - '!/^ERROR:/' diff --git a/testdata/matching_transformers.yaml b/testdata/matching_transformers.yaml new file mode 100644 index 00000000..e3010071 --- /dev/null +++ b/testdata/matching_transformers.yaml @@ -0,0 +1,129 @@ +matching: + basic_reader_as_array: + as-reader: true + content: | + foo bar + moo cow + matches: + and: + - contain-element: {contain-substring: 'foo'} + - contain-element: {match-regexp: '^m.*w$'} + - not: {contain-substring: 'ftw'} + - not: {match-regexp: '^ERROR:'} + + test_numeric_string: + content: 128 + matches: + and: + - '128' + - have-prefix: '1' + - have-suffix: '8' + - match-regexp: '\d{3}' + + test_string_numeric: + content: '128' + matches: + and: + - 128 + - 128.0 + - le: 128 + - gt: 120 + + test_string_float: + content: '128.3' + matches: + and: + - 128.3 + - le: 129 + - gt: 120.2 + + test_array: + content: + - '45' + - '46' + - '47' + matches: + - contain-element: {match-regexp: "4."} + - '45' + - and: [{ge: 46}, {le: 50}] + + test_reader_using_string_matchers: + content: | + foo bar + 15 + moo cow + as-reader: true + matches: + and: + - have-len: 19 + - | + foo bar + 15 + moo cow + - have-prefix: 'foo' + - have-suffix: "cow\n" + - contain-element: + have-prefix: 'moo' + - contain-elements: + - not: 'this_doesnt_exist' + - lt: 20 + - have-prefix: 'moo' + + test_reader_using_array: + content: | + foo bar + 15 + moo cow + as-reader: true + matches: + - "foo bar" + - "15" + - "moo cow" + + + test_reader_as_single_string: + content: 'cool' + as-reader: true + matches: 'cool' + + test_reader_using_int_matchers: + content: '40' + as-reader: true + matches: + and: + - le: 250 + - ge: 20 + + + test_gjson_transform: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + moo.nested: cow + foo: {have-prefix: b} + count: {le: 25} + '@this': {have-key: "foo"} + moo: + and: + - have-key: "nested" + - {not: {have-key: "nested2"}} + + test_gjson_using_this_and_equal: + content: '{"foo": "bar", "baz": "bing"}' + matches: + gjson: + '@this': + equal: + foo: bar + baz: bing + + test_gjson_have_key_array: + content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}' + matches: + gjson: + arr: + # or tests MarshalJSON + or: + - contain-elements: + - have-key: 'nested' diff --git a/testdata/matching_transformers_failing.yaml b/testdata/matching_transformers_failing.yaml new file mode 100644 index 00000000..f49482ba --- /dev/null +++ b/testdata/matching_transformers_failing.yaml @@ -0,0 +1,162 @@ +matching: + basic_reader_as_array: + as-reader: true + content: | + foo bar + moo cow + matches: + and: + - contain-element: {contain-substring: 'fox'} + - contain-element: {match-regexp: '^t.*w$'} + - not: {contain-substring: 'foo'} + - not: {match-regexp: '^foo'} + + test_numeric_string: + content: 128 + matches: + and: + - '129' + - have-prefix: '2' + - have-suffix: '9' + - match-regexp: '\s{3}' + + test_string_numeric: + content: '128' + matches: + and: + - 129 + - 129.1 + - le: 127 + - gt: 130 + + test_string_float: + content: '128.3' + matches: + and: + - 129.3 + - le: 127 + - gt: 130.2 + + test_array: + content: + - '45' + - '46' + - '47' + matches: + - contain-element: {match-regexp: "5."} + - '55' + - and: [{ge: 56}, {le: 30}] + + test_reader_using_string_matchers: + content: | + foo bar + 15 + moo cow + as-reader: true + matches: + and: + - have-len: 15 + - | + fox bar + 15 + moo cow + - have-prefix: 'fox' + - have-suffix: "tow\n" + - contain-element: + have-prefix: 'too' + - contain-elements: + - not: 'moo cow' + - lt: 10 + - have-prefix: 'tow' + + + test_reader_as_single_string: + content: 'cool' + as-reader: true + matches: 'not-cool' + + test_reader_using_int_matchers: + content: '40' + as-reader: true + matches: + and: + - le: 20 + - ge: 50 + + + test_gjson_transform_simple: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + moo.nested: cowx + + test_gjson_transform_nested_prefix: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + foo: {have-prefix: x} + + test_gjson_transform_nested_count: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + count: {le: 10} + + test_gjson_transform_nested_this: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + '@this': {have-key: "nope"} + + test_gjson_transform_nested_and: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + moo: + and: + - {have-key: "nope"} + - {not: {have-key: "nested"}} + + test_gjson_transform_not_key: + content: '{"foo": "bar", "moo": {"nested": "cow"}, "count": "15"}' + as-reader: true + matches: + gjson: + moo: + not: + have-key: "nested" + + test_gjson_using_this_and_equal: + content: '{"foo": "bar", "baz": "bing"}' + matches: + gjson: + '@this': + equal: + fox: bar + baz: bing + + test_gjson_have_key_array: + content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}' + matches: + gjson: + '@this': + or: + - have-key: "fail" + + test_gjson_not_found: + content: '{"arr": [{"nested": "cow"}, {"nested2": "moo"}]}' + matches: + gjson: + foo: 'bar' + + test_gjson_invalid: + content: '{"arr"' + matches: + gjson: + '@this': + - have-key: "arr" diff --git a/testdata/out_matching_basic.0.documentation b/testdata/out_matching_basic.0.documentation new file mode 100644 index 00000000..d96a8e27 --- /dev/null +++ b/testdata/out_matching_basic.0.documentation @@ -0,0 +1,22 @@ +Matching: basic_array: matches: matches expectation: ["group1","group2"] +Matching: basic_array_matchers: matches: matches expectation: {"and":[{"contain-elements":["foo","bar"]},["foo","bar"],{"equal":["foo","bar","moo"]},{"consist-of":["foo",{"have-prefix":"m"},"bar"]},{"contain-element":{"have-prefix":"b"}},{"contain-element":{"have-suffix":"r"}}]} +Matching: basic_int: matches: matches expectation: 42 +Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"] +Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"} +Matching: basic_string: matches: matches expectation: "this is a test" +Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"} +Matching: basic_string_skip: matches: skipped +Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]} +Matching: negated_basic_array_matchers: matches: matches expectation: {"and":[{"not":{"contain-elements":["fox","box"]}},{"not":["fox","bax"]},{"not":{"equal":["fox","bax","mox"]}},{"not":{"consist-of":[{"have-suffix":"x"},{"have-prefix":"t"},"box"]}},{"not":{"contain-element":{"have-prefix":"x"}}}]} +Matching: negated_basic_int: matches: matches expectation: {"not":43} +Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}} +Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"} +Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}} + + +Failures/Skipped: + +Matching: basic_string_skip: matches: skipped + +Total Duration: +Count: 14, Failed: 0, Skipped: 1 diff --git a/testdata/out_matching_basic.0.nagios b/testdata/out_matching_basic.0.nagios new file mode 100644 index 00000000..fc2d28ea --- /dev/null +++ b/testdata/out_matching_basic.0.nagios @@ -0,0 +1 @@ +GOSS OK - Count: 14, Failed: 0, Skipped: 1, Duration: diff --git a/testdata/out_matching_basic.0.rspecish b/testdata/out_matching_basic.0.rspecish new file mode 100644 index 00000000..3d8077bb --- /dev/null +++ b/testdata/out_matching_basic.0.rspecish @@ -0,0 +1,8 @@ +.......S...... + +Failures/Skipped: + +Matching: basic_string_skip: matches: skipped + +Total Duration: +Count: 14, Failed: 0, Skipped: 1 diff --git a/testdata/out_matching_basic.0.tap b/testdata/out_matching_basic.0.tap new file mode 100644 index 00000000..0ab691b7 --- /dev/null +++ b/testdata/out_matching_basic.0.tap @@ -0,0 +1,15 @@ +1..14 +ok 1 - Matching: basic_array: matches: matches expectation: ["group1","group2"] +ok 2 - Matching: basic_array_matchers: matches: matches expectation: {"and":[{"contain-elements":["foo","bar"]},["foo","bar"],{"equal":["foo","bar","moo"]},{"consist-of":["foo",{"have-prefix":"m"},"bar"]},{"contain-element":{"have-prefix":"b"}},{"contain-element":{"have-suffix":"r"}}]} +ok 3 - Matching: basic_int: matches: matches expectation: 42 +ok 4 - Matching: basic_reader: matches: matches expectation: ["foo","/^m.*w$/","!ftw","!/^ERROR:/"] +ok 5 - Matching: basic_semver: matches: matches expectation: {"semver-constraint":">=1.2.0"} +ok 6 - Matching: basic_string: matches: matches expectation: "this is a test" +ok 7 - Matching: basic_string_regexp: matches: matches expectation: {"match-regexp":"^this"} +ok 8 - # SKIP Matching: basic_string_skip: matches: skipped +ok 9 - Matching: negated_basic_array: matches: matches expectation: {"not":["group1","group2","group2","group4"]} +ok 10 - Matching: negated_basic_array_matchers: matches: matches expectation: {"and":[{"not":{"contain-elements":["fox","box"]}},{"not":["fox","bax"]},{"not":{"equal":["fox","bax","mox"]}},{"not":{"consist-of":[{"have-suffix":"x"},{"have-prefix":"t"},"box"]}},{"not":{"contain-element":{"have-prefix":"x"}}}]} +ok 11 - Matching: negated_basic_int: matches: matches expectation: {"not":43} +ok 12 - Matching: negated_basic_reader: matches: matches expectation: {"not":{"contain-elements":["fox","/^t.*w$/","!foo","!/^foo/"]}} +ok 13 - Matching: negated_basic_string: matches: matches expectation: {"not":"this is a failing test"} +ok 14 - Matching: negated_basic_string_regexp: matches: matches expectation: {"not":{"match-regexp":"^foo"}} diff --git a/testdata/out_matching_basic_failing.1.documentation b/testdata/out_matching_basic_failing.1.documentation new file mode 100644 index 00000000..3e6406f2 --- /dev/null +++ b/testdata/out_matching_basic_failing.1.documentation @@ -0,0 +1,299 @@ +Matching: basic_array: matches: +Expected + ["group1","group2","group3"] +to contain elements + ["group1","group2","group2","group4"] +the missing elements were + ["group2","group4"] +Matching: basic_array_consists_of: matches: +Expected + ["foo","bar","moo"] +to consist of + ["fox",{"have-prefix":"t"},"box"] +the missing elements were + ["fox",{"have-prefix":"t"},"box"] +the extra elements were + ["foo","bar","moo"] +Matching: basic_array_matchers: matches: +Expected + ["foo","bar","moo"] +to satisfy at least one of these matchers + [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}] +Matching: basic_int: matches: +Expected + 42 +to be numerically eq + 43 +Matching: basic_len: matches: +Expected + "123" +to satisfy at least one of these matchers + [{"have-len":2}] +Matching: basic_reader: matches: +Expected + "object: *strings.Reader" +to have patterns + ["fox","/^t.*w$/","!foo","!/^foo/"] +the missing elements were + ["fox","/^t.*w$/","!foo","!/^foo/"] +Matching: basic_semver: matches: +Expected + "1.2.3" +to satisfy at least one of these matchers + [{"semver-constraint":">=9.9.0"}] +Matching: basic_string: matches: +Expected + "this is a test" +to equal + "this is a failing test" +Matching: basic_string_contain_substring: matches: +Expected + "foo" +to contain substring + "x" +Matching: basic_string_have_prefix: matches: +Expected + "foo" +to have prefix + "g" +Matching: basic_string_have_suffix: matches: +Expected + "foo" +to have suffix + "x" +Matching: basic_string_regexp: matches: +Expected + "this is a test" +to match regular expression + "^foo" +Matching: negated_and: matches: +Expected + 42 +not to satisfy all of these matchers + [{"eq":42},{"eq":42}] +Matching: negated_basic_array: matches: +Expected + ["group1","group2","group3"] +not to contain elements + ["group1","group2","group3"] +Matching: negated_basic_array_consists_of: matches: +Expected + ["foo","bar","moo"] +not to consist of + ["foo",{"have-prefix":"m"},"bar"] +Matching: negated_basic_array_contain_element: matches: +Expected + ["foo","bar","moo"] +not to contain element matching + "foo" +Matching: negated_basic_array_matchers: matches: +Expected + ["foo","bar","moo"] +to satisfy at least one of these matchers + [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}] +Matching: negated_basic_int: matches: +Expected + 42 +not to be numerically eq + 42 +Matching: negated_basic_reader: matches: +Error + ContainElements matcher expects an array/slice/map. Got: + : foo bar + moo cow + +Matching: negated_basic_string: matches: +Expected + "this is a test" +not to equal + "this is a test" +Matching: negatedbasic_len: matches: +Expected + "123" +not to have length + 3 +Matching: negatedbasic_string_contain_substring: matches: +Expected + "foo" +not to contain substring + "oo" +Matching: negatedbasic_string_have_prefix: matches: +Expected + "foo" +not to have prefix + "f" +Matching: negatedbasic_string_have_suffix: matches: +Expected + "foo" +not to have suffix + "o" +Matching: negatedbasic_string_regexp: matches: +Expected + "this is a test" +not to match regular expression + "^this" + + +Failures/Skipped: + +Matching: basic_array: matches: +Expected + ["group1","group2","group3"] +to contain elements + ["group1","group2","group2","group4"] +the missing elements were + ["group2","group4"] + +Matching: basic_array_consists_of: matches: +Expected + ["foo","bar","moo"] +to consist of + ["fox",{"have-prefix":"t"},"box"] +the missing elements were + ["fox",{"have-prefix":"t"},"box"] +the extra elements were + ["foo","bar","moo"] + +Matching: basic_array_matchers: matches: +Expected + ["foo","bar","moo"] +to satisfy at least one of these matchers + [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}] + +Matching: basic_int: matches: +Expected + 42 +to be numerically eq + 43 + +Matching: basic_len: matches: +Expected + "123" +to satisfy at least one of these matchers + [{"have-len":2}] + +Matching: basic_reader: matches: +Expected + "object: *strings.Reader" +to have patterns + ["fox","/^t.*w$/","!foo","!/^foo/"] +the missing elements were + ["fox","/^t.*w$/","!foo","!/^foo/"] + +Matching: basic_semver: matches: +Expected + "1.2.3" +to satisfy at least one of these matchers + [{"semver-constraint":">=9.9.0"}] + +Matching: basic_string: matches: +Expected + "this is a test" +to equal + "this is a failing test" + +Matching: basic_string_contain_substring: matches: +Expected + "foo" +to contain substring + "x" + +Matching: basic_string_have_prefix: matches: +Expected + "foo" +to have prefix + "g" + +Matching: basic_string_have_suffix: matches: +Expected + "foo" +to have suffix + "x" + +Matching: basic_string_regexp: matches: +Expected + "this is a test" +to match regular expression + "^foo" + +Matching: negated_and: matches: +Expected + 42 +not to satisfy all of these matchers + [{"eq":42},{"eq":42}] + +Matching: negated_basic_array: matches: +Expected + ["group1","group2","group3"] +not to contain elements + ["group1","group2","group3"] + +Matching: negated_basic_array_consists_of: matches: +Expected + ["foo","bar","moo"] +not to consist of + ["foo",{"have-prefix":"m"},"bar"] + +Matching: negated_basic_array_contain_element: matches: +Expected + ["foo","bar","moo"] +not to contain element matching + "foo" + +Matching: negated_basic_array_matchers: matches: +Expected + ["foo","bar","moo"] +to satisfy at least one of these matchers + [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}] + +Matching: negated_basic_int: matches: +Expected + 42 +not to be numerically eq + 42 + +Matching: negated_basic_reader: matches: +Error + ContainElements matcher expects an array/slice/map. Got: + : foo bar + moo cow + + +Matching: negated_basic_string: matches: +Expected + "this is a test" +not to equal + "this is a test" + +Matching: negatedbasic_len: matches: +Expected + "123" +not to have length + 3 + +Matching: negatedbasic_string_contain_substring: matches: +Expected + "foo" +not to contain substring + "oo" + +Matching: negatedbasic_string_have_prefix: matches: +Expected + "foo" +not to have prefix + "f" + +Matching: negatedbasic_string_have_suffix: matches: +Expected + "foo" +not to have suffix + "o" + +Matching: negatedbasic_string_regexp: matches: +Expected + "this is a test" +not to match regular expression + "^this" + +Total Duration: +Count: 25, Failed: 25, Skipped: 0 diff --git a/testdata/out_matching_basic_failing.1.rspecish b/testdata/out_matching_basic_failing.1.rspecish new file mode 100644 index 00000000..8618ec70 --- /dev/null +++ b/testdata/out_matching_basic_failing.1.rspecish @@ -0,0 +1,165 @@ +FFFFFFFFFFFFFFFFFFFFFFFFF + +Failures/Skipped: + +Matching: basic_array: matches: +Expected + ["group1","group2","group3"] +to contain elements + ["group1","group2","group2","group4"] +the missing elements were + ["group2","group4"] + +Matching: basic_array_consists_of: matches: +Expected + ["foo","bar","moo"] +to consist of + ["fox",{"have-prefix":"t"},"box"] +the missing elements were + ["fox",{"have-prefix":"t"},"box"] +the extra elements were + ["foo","bar","moo"] + +Matching: basic_array_matchers: matches: +Expected + ["foo","bar","moo"] +to satisfy at least one of these matchers + [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}] + +Matching: basic_int: matches: +Expected + 42 +to be numerically eq + 43 + +Matching: basic_len: matches: +Expected + "123" +to satisfy at least one of these matchers + [{"have-len":2}] + +Matching: basic_reader: matches: +Expected + "object: *strings.Reader" +to have patterns + ["fox","/^t.*w$/","!foo","!/^foo/"] +the missing elements were + ["fox","/^t.*w$/","!foo","!/^foo/"] + +Matching: basic_semver: matches: +Expected + "1.2.3" +to satisfy at least one of these matchers + [{"semver-constraint":">=9.9.0"}] + +Matching: basic_string: matches: +Expected + "this is a test" +to equal + "this is a failing test" + +Matching: basic_string_contain_substring: matches: +Expected + "foo" +to contain substring + "x" + +Matching: basic_string_have_prefix: matches: +Expected + "foo" +to have prefix + "g" + +Matching: basic_string_have_suffix: matches: +Expected + "foo" +to have suffix + "x" + +Matching: basic_string_regexp: matches: +Expected + "this is a test" +to match regular expression + "^foo" + +Matching: negated_and: matches: +Expected + 42 +not to satisfy all of these matchers + [{"eq":42},{"eq":42}] + +Matching: negated_basic_array: matches: +Expected + ["group1","group2","group3"] +not to contain elements + ["group1","group2","group3"] + +Matching: negated_basic_array_consists_of: matches: +Expected + ["foo","bar","moo"] +not to consist of + ["foo",{"have-prefix":"m"},"bar"] + +Matching: negated_basic_array_contain_element: matches: +Expected + ["foo","bar","moo"] +not to contain element matching + "foo" + +Matching: negated_basic_array_matchers: matches: +Expected + ["foo","bar","moo"] +to satisfy at least one of these matchers + [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}] + +Matching: negated_basic_int: matches: +Expected + 42 +not to be numerically eq + 42 + +Matching: negated_basic_reader: matches: +Error + ContainElements matcher expects an array/slice/map. Got: + : foo bar + moo cow + + +Matching: negated_basic_string: matches: +Expected + "this is a test" +not to equal + "this is a test" + +Matching: negatedbasic_len: matches: +Expected + "123" +not to have length + 3 + +Matching: negatedbasic_string_contain_substring: matches: +Expected + "foo" +not to contain substring + "oo" + +Matching: negatedbasic_string_have_prefix: matches: +Expected + "foo" +not to have prefix + "f" + +Matching: negatedbasic_string_have_suffix: matches: +Expected + "foo" +not to have suffix + "o" + +Matching: negatedbasic_string_regexp: matches: +Expected + "this is a test" +not to match regular expression + "^this" + +Total Duration: +Count: 25, Failed: 25, Skipped: 0 diff --git a/testdata/out_matching_basic_failing.1.tap b/testdata/out_matching_basic_failing.1.tap new file mode 100644 index 00000000..1e7b141b --- /dev/null +++ b/testdata/out_matching_basic_failing.1.tap @@ -0,0 +1,26 @@ +1..25 +not ok 1 - Matching: basic_array: matches: Expected ["group1","group2","group3"] to contain elements ["group1","group2","group2","group4"] the missing elements were ["group2","group4"] +not ok 2 - Matching: basic_array_consists_of: matches: Expected ["foo","bar","moo"] to consist of ["fox",{"have-prefix":"t"},"box"] the missing elements were ["fox",{"have-prefix":"t"},"box"] the extra elements were ["foo","bar","moo"] +not ok 3 - Matching: basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"contain-elements":["fox","box"]},{"contain-elements":["fox","bax"]},["fox","bax","mox"],{"consist-of":["fox",{"have-prefix":"t"},"box"]},{"contain-element":{"have-prefix":"x"}},{"contain-element":{"have-suffix":"x"}}] +not ok 4 - Matching: basic_int: matches: Expected 42 to be numerically eq 43 +not ok 5 - Matching: basic_len: matches: Expected "123" to satisfy at least one of these matchers [{"have-len":2}] +not ok 6 - Matching: basic_reader: matches: Expected "object: *strings.Reader" to have patterns ["fox","/^t.*w$/","!foo","!/^foo/"] the missing elements were ["fox","/^t.*w$/","!foo","!/^foo/"] +not ok 7 - Matching: basic_semver: matches: Expected "1.2.3" to satisfy at least one of these matchers [{"semver-constraint":">=9.9.0"}] +not ok 8 - Matching: basic_string: matches: Expected "this is a test" to equal "this is a failing test" +not ok 9 - Matching: basic_string_contain_substring: matches: Expected "foo" to contain substring "x" +not ok 10 - Matching: basic_string_have_prefix: matches: Expected "foo" to have prefix "g" +not ok 11 - Matching: basic_string_have_suffix: matches: Expected "foo" to have suffix "x" +not ok 12 - Matching: basic_string_regexp: matches: Expected "this is a test" to match regular expression "^foo" +not ok 13 - Matching: negated_and: matches: Expected 42 not to satisfy all of these matchers [{"eq":42},{"eq":42}] +not ok 14 - Matching: negated_basic_array: matches: Expected ["group1","group2","group3"] not to contain elements ["group1","group2","group3"] +not ok 15 - Matching: negated_basic_array_consists_of: matches: Expected ["foo","bar","moo"] not to consist of ["foo",{"have-prefix":"m"},"bar"] +not ok 16 - Matching: negated_basic_array_contain_element: matches: Expected ["foo","bar","moo"] not to contain element matching "foo" +not ok 17 - Matching: negated_basic_array_matchers: matches: Expected ["foo","bar","moo"] to satisfy at least one of these matchers [{"not":{"contain-elements":["foo","bar"]}},{"not":{"contain-elements":["foo","bar"]}},{"not":["foo","bar","moo"]},{"not":{"consist-of":["foo",{"have-prefix":"m"},"bar"]}},{"not":{"contain-element":{"have-prefix":"b"}}}] +not ok 18 - Matching: negated_basic_int: matches: Expected 42 not to be numerically eq 42 +not ok 19 - Matching: negated_basic_reader: matches: Error ContainElements matcher expects an array/slice/map. Got: : foo bar moo cow +not ok 20 - Matching: negated_basic_string: matches: Expected "this is a test" not to equal "this is a test" +not ok 21 - Matching: negatedbasic_len: matches: Expected "123" not to have length 3 +not ok 22 - Matching: negatedbasic_string_contain_substring: matches: Expected "foo" not to contain substring "oo" +not ok 23 - Matching: negatedbasic_string_have_prefix: matches: Expected "foo" not to have prefix "f" +not ok 24 - Matching: negatedbasic_string_have_suffix: matches: Expected "foo" not to have suffix "o" +not ok 25 - Matching: negatedbasic_string_regexp: matches: Expected "this is a test" not to match regular expression "^this" diff --git a/testdata/out_matching_basic_failing.2.nagios b/testdata/out_matching_basic_failing.2.nagios new file mode 100644 index 00000000..aa3ddac5 --- /dev/null +++ b/testdata/out_matching_basic_failing.2.nagios @@ -0,0 +1 @@ +GOSS CRITICAL - Count: 25, Failed: 25, Skipped: 0, Duration: diff --git a/testdata/out_matching_transformers.0.documentation b/testdata/out_matching_transformers.0.documentation new file mode 100644 index 00000000..136a6449 --- /dev/null +++ b/testdata/out_matching_transformers.0.documentation @@ -0,0 +1,16 @@ +Matching: basic_reader_as_array: matches: matches expectation: {"and":[{"contain-element":{"contain-substring":"foo"}},{"contain-element":{"match-regexp":"^m.*w$"}},{"not":{"contain-substring":"ftw"}},{"not":{"match-regexp":"^ERROR:"}}]} +Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}] +Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}} +Matching: test_gjson_transform: matches: matches expectation: {"gjson":{"@this":{"have-key":"foo"},"count":{"le":25},"foo":{"have-prefix":"b"},"moo":{"and":[{"have-key":"nested"},{"not":{"have-key":"nested2"}}]},"moo.nested":"cow"}} +Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}} +Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]} +Matching: test_reader_as_single_string: matches: matches expectation: "cool" +Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"] +Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]} +Matching: test_reader_using_string_matchers: matches: matches expectation: {"and":[{"have-len":19},"foo bar\n15\nmoo cow\n",{"have-prefix":"foo"},{"have-suffix":"cow\n"},{"contain-element":{"have-prefix":"moo"}},{"contain-elements":[{"not":"this_doesnt_exist"},{"lt":20},{"have-prefix":"moo"}]}]} +Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]} +Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]} + + +Total Duration: +Count: 12, Failed: 0, Skipped: 0 diff --git a/testdata/out_matching_transformers.0.nagios b/testdata/out_matching_transformers.0.nagios new file mode 100644 index 00000000..749fcaed --- /dev/null +++ b/testdata/out_matching_transformers.0.nagios @@ -0,0 +1 @@ +GOSS OK - Count: 12, Failed: 0, Skipped: 0, Duration: diff --git a/testdata/out_matching_transformers.0.rspecish b/testdata/out_matching_transformers.0.rspecish new file mode 100644 index 00000000..a5b03e65 --- /dev/null +++ b/testdata/out_matching_transformers.0.rspecish @@ -0,0 +1,4 @@ +............ + +Total Duration: +Count: 12, Failed: 0, Skipped: 0 diff --git a/testdata/out_matching_transformers.0.tap b/testdata/out_matching_transformers.0.tap new file mode 100644 index 00000000..e01c41d1 --- /dev/null +++ b/testdata/out_matching_transformers.0.tap @@ -0,0 +1,13 @@ +1..12 +ok 1 - Matching: basic_reader_as_array: matches: matches expectation: {"and":[{"contain-element":{"contain-substring":"foo"}},{"contain-element":{"match-regexp":"^m.*w$"}},{"not":{"contain-substring":"ftw"}},{"not":{"match-regexp":"^ERROR:"}}]} +ok 2 - Matching: test_array: matches: matches expectation: [{"contain-element":{"match-regexp":"4."}},"45",{"and":[{"ge":46},{"le":50}]}] +ok 3 - Matching: test_gjson_have_key_array: matches: matches expectation: {"gjson":{"arr":{"or":[{"contain-elements":[{"have-key":"nested"}]}]}}} +ok 4 - Matching: test_gjson_transform: matches: matches expectation: {"gjson":{"@this":{"have-key":"foo"},"count":{"le":25},"foo":{"have-prefix":"b"},"moo":{"and":[{"have-key":"nested"},{"not":{"have-key":"nested2"}}]},"moo.nested":"cow"}} +ok 5 - Matching: test_gjson_using_this_and_equal: matches: matches expectation: {"gjson":{"@this":{"equal":{"baz":"bing","foo":"bar"}}}} +ok 6 - Matching: test_numeric_string: matches: matches expectation: {"and":["128",{"have-prefix":"1"},{"have-suffix":"8"},{"match-regexp":"\\d{3}"}]} +ok 7 - Matching: test_reader_as_single_string: matches: matches expectation: "cool" +ok 8 - Matching: test_reader_using_array: matches: matches expectation: ["foo bar","15","moo cow"] +ok 9 - Matching: test_reader_using_int_matchers: matches: matches expectation: {"and":[{"le":250},{"ge":20}]} +ok 10 - Matching: test_reader_using_string_matchers: matches: matches expectation: {"and":[{"have-len":19},"foo bar\n15\nmoo cow\n",{"have-prefix":"foo"},{"have-suffix":"cow\n"},{"contain-element":{"have-prefix":"moo"}},{"contain-elements":[{"not":"this_doesnt_exist"},{"lt":20},{"have-prefix":"moo"}]}]} +ok 11 - Matching: test_string_float: matches: matches expectation: {"and":[128.3,{"le":129},{"gt":120.2}]} +ok 12 - Matching: test_string_numeric: matches: matches expectation: {"and":[128,128,{"le":128},{"gt":120}]} diff --git a/testdata/out_matching_transformers_failing.1.documentation b/testdata/out_matching_transformers_failing.1.documentation new file mode 100644 index 00000000..c80967b3 --- /dev/null +++ b/testdata/out_matching_transformers_failing.1.documentation @@ -0,0 +1,320 @@ +Matching: basic_reader_as_array: matches: +Expected + ["foo bar","moo cow",""] +to contain element matching + {"contain-substring":"fox"} +the transform chain was + [{"to-array":{}}] +the raw value was + "foo bar\nmoo cow\n" +Matching: test_array: matches: +Expected + ["45","46","47"] +to contain elements + [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] +the missing elements were + [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] +Matching: test_gjson_have_key_array: matches: +Expected + {"arr":[{"nested":"cow"},{"nested2":"moo"}]} +to satisfy at least one of these matchers + [{"have-key":"fail"}] +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" +Matching: test_gjson_invalid: matches: +Error + matchers.Gjson{Path:"@this"}: Invalid json +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"arr\"" +Matching: test_gjson_not_found: matches: +Error + matchers.Gjson{Path:"foo"}: Path not found: foo +the transform chain was + [{"gjson":{"Path":"foo"}}] +the raw value was + "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" +Matching: test_gjson_transform_nested_and: matches: +Expected + {"nested":"cow"} +to have key matching + "nope" +the transform chain was + [{"gjson":{"Path":"moo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +Matching: test_gjson_transform_nested_count: matches: +Expected + 15 +to be numerically le + 10 +the transform chain was + [{"gjson":{"Path":"count"}},{"to-numeric":{}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +Matching: test_gjson_transform_nested_prefix: matches: +Expected + "bar" +to have prefix + "x" +the transform chain was + [{"gjson":{"Path":"foo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +Matching: test_gjson_transform_nested_this: matches: +Expected + {"count":"15","foo":"bar","moo":{"nested":"cow"}} +to have key matching + "nope" +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +Matching: test_gjson_transform_not_key: matches: +Expected + {"nested":"cow"} +not to have key matching + "nested" +the transform chain was + [{"gjson":{"Path":"moo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +Matching: test_gjson_transform_simple: matches: +Expected + "cow" +to equal + "cowx" +the transform chain was + [{"gjson":{"Path":"moo.nested"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +Matching: test_gjson_using_this_and_equal: matches: +Expected + {"baz":"bing","foo":"bar"} +to equal + {"baz":"bing","fox":"bar"} +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"foo\": \"bar\", \"baz\": \"bing\"}" +Matching: test_numeric_string: matches: +Expected + "128" +to equal + "129" +the transform chain was + [{"to-string":{}}] +the raw value was + 128 +Matching: test_reader_as_single_string: matches: +Expected + "cool" +to equal + "not-cool" +Matching: test_reader_using_int_matchers: matches: +Expected + 40 +to be numerically le + 20 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "40" +Matching: test_reader_using_string_matchers: matches: +Expected + "foo bar\n15\nmoo cow\n" +to have length + 15 +Matching: test_string_float: matches: +Expected + 128.3 +to be numerically eq + 129.3 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "128.3" +Matching: test_string_numeric: matches: +Expected + 128 +to be numerically eq + 129 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "128" + + +Failures/Skipped: + +Matching: basic_reader_as_array: matches: +Expected + ["foo bar","moo cow",""] +to contain element matching + {"contain-substring":"fox"} +the transform chain was + [{"to-array":{}}] +the raw value was + "foo bar\nmoo cow\n" + +Matching: test_array: matches: +Expected + ["45","46","47"] +to contain elements + [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] +the missing elements were + [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] + +Matching: test_gjson_have_key_array: matches: +Expected + {"arr":[{"nested":"cow"},{"nested2":"moo"}]} +to satisfy at least one of these matchers + [{"have-key":"fail"}] +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" + +Matching: test_gjson_invalid: matches: +Error + matchers.Gjson{Path:"@this"}: Invalid json +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"arr\"" + +Matching: test_gjson_not_found: matches: +Error + matchers.Gjson{Path:"foo"}: Path not found: foo +the transform chain was + [{"gjson":{"Path":"foo"}}] +the raw value was + "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" + +Matching: test_gjson_transform_nested_and: matches: +Expected + {"nested":"cow"} +to have key matching + "nope" +the transform chain was + [{"gjson":{"Path":"moo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_nested_count: matches: +Expected + 15 +to be numerically le + 10 +the transform chain was + [{"gjson":{"Path":"count"}},{"to-numeric":{}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_nested_prefix: matches: +Expected + "bar" +to have prefix + "x" +the transform chain was + [{"gjson":{"Path":"foo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_nested_this: matches: +Expected + {"count":"15","foo":"bar","moo":{"nested":"cow"}} +to have key matching + "nope" +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_not_key: matches: +Expected + {"nested":"cow"} +not to have key matching + "nested" +the transform chain was + [{"gjson":{"Path":"moo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_simple: matches: +Expected + "cow" +to equal + "cowx" +the transform chain was + [{"gjson":{"Path":"moo.nested"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_using_this_and_equal: matches: +Expected + {"baz":"bing","foo":"bar"} +to equal + {"baz":"bing","fox":"bar"} +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"foo\": \"bar\", \"baz\": \"bing\"}" + +Matching: test_numeric_string: matches: +Expected + "128" +to equal + "129" +the transform chain was + [{"to-string":{}}] +the raw value was + 128 + +Matching: test_reader_as_single_string: matches: +Expected + "cool" +to equal + "not-cool" + +Matching: test_reader_using_int_matchers: matches: +Expected + 40 +to be numerically le + 20 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "40" + +Matching: test_reader_using_string_matchers: matches: +Expected + "foo bar\n15\nmoo cow\n" +to have length + 15 + +Matching: test_string_float: matches: +Expected + 128.3 +to be numerically eq + 129.3 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "128.3" + +Matching: test_string_numeric: matches: +Expected + 128 +to be numerically eq + 129 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "128" + +Total Duration: +Count: 18, Failed: 18, Skipped: 0 diff --git a/testdata/out_matching_transformers_failing.1.rspecish b/testdata/out_matching_transformers_failing.1.rspecish new file mode 100644 index 00000000..273492d6 --- /dev/null +++ b/testdata/out_matching_transformers_failing.1.rspecish @@ -0,0 +1,172 @@ +FFFFFFFFFFFFFFFFFF + +Failures/Skipped: + +Matching: basic_reader_as_array: matches: +Expected + ["foo bar","moo cow",""] +to contain element matching + {"contain-substring":"fox"} +the transform chain was + [{"to-array":{}}] +the raw value was + "foo bar\nmoo cow\n" + +Matching: test_array: matches: +Expected + ["45","46","47"] +to contain elements + [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] +the missing elements were + [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] + +Matching: test_gjson_have_key_array: matches: +Expected + {"arr":[{"nested":"cow"},{"nested2":"moo"}]} +to satisfy at least one of these matchers + [{"have-key":"fail"}] +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" + +Matching: test_gjson_invalid: matches: +Error + matchers.Gjson{Path:"@this"}: Invalid json +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"arr\"" + +Matching: test_gjson_not_found: matches: +Error + matchers.Gjson{Path:"foo"}: Path not found: foo +the transform chain was + [{"gjson":{"Path":"foo"}}] +the raw value was + "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" + +Matching: test_gjson_transform_nested_and: matches: +Expected + {"nested":"cow"} +to have key matching + "nope" +the transform chain was + [{"gjson":{"Path":"moo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_nested_count: matches: +Expected + 15 +to be numerically le + 10 +the transform chain was + [{"gjson":{"Path":"count"}},{"to-numeric":{}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_nested_prefix: matches: +Expected + "bar" +to have prefix + "x" +the transform chain was + [{"gjson":{"Path":"foo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_nested_this: matches: +Expected + {"count":"15","foo":"bar","moo":{"nested":"cow"}} +to have key matching + "nope" +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_not_key: matches: +Expected + {"nested":"cow"} +not to have key matching + "nested" +the transform chain was + [{"gjson":{"Path":"moo"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_transform_simple: matches: +Expected + "cow" +to equal + "cowx" +the transform chain was + [{"gjson":{"Path":"moo.nested"}}] +the raw value was + "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" + +Matching: test_gjson_using_this_and_equal: matches: +Expected + {"baz":"bing","foo":"bar"} +to equal + {"baz":"bing","fox":"bar"} +the transform chain was + [{"gjson":{"Path":"@this"}}] +the raw value was + "{\"foo\": \"bar\", \"baz\": \"bing\"}" + +Matching: test_numeric_string: matches: +Expected + "128" +to equal + "129" +the transform chain was + [{"to-string":{}}] +the raw value was + 128 + +Matching: test_reader_as_single_string: matches: +Expected + "cool" +to equal + "not-cool" + +Matching: test_reader_using_int_matchers: matches: +Expected + 40 +to be numerically le + 20 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "40" + +Matching: test_reader_using_string_matchers: matches: +Expected + "foo bar\n15\nmoo cow\n" +to have length + 15 + +Matching: test_string_float: matches: +Expected + 128.3 +to be numerically eq + 129.3 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "128.3" + +Matching: test_string_numeric: matches: +Expected + 128 +to be numerically eq + 129 +the transform chain was + [{"to-numeric":{}}] +the raw value was + "128" + +Total Duration: +Count: 18, Failed: 18, Skipped: 0 diff --git a/testdata/out_matching_transformers_failing.1.tap b/testdata/out_matching_transformers_failing.1.tap new file mode 100644 index 00000000..851a8c6a --- /dev/null +++ b/testdata/out_matching_transformers_failing.1.tap @@ -0,0 +1,19 @@ +1..18 +not ok 1 - Matching: basic_reader_as_array: matches: Expected ["foo bar","moo cow",""] to contain element matching {"contain-substring":"fox"} the transform chain was [{"to-array":{}}] the raw value was "foo bar\nmoo cow\n" +not ok 2 - Matching: test_array: matches: Expected ["45","46","47"] to contain elements [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] the missing elements were [{"contain-element":{"match-regexp":"5."}},"55",{"and":[{"ge":56},{"le":30}]}] +not ok 3 - Matching: test_gjson_have_key_array: matches: Expected {"arr":[{"nested":"cow"},{"nested2":"moo"}]} to satisfy at least one of these matchers [{"have-key":"fail"}] the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" +not ok 4 - Matching: test_gjson_invalid: matches: Error matchers.Gjson{Path:"@this"}: Invalid json the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"arr\"" +not ok 5 - Matching: test_gjson_not_found: matches: Error matchers.Gjson{Path:"foo"}: Path not found: foo the transform chain was [{"gjson":{"Path":"foo"}}] the raw value was "{\"arr\": [{\"nested\": \"cow\"}, {\"nested2\": \"moo\"}]}" +not ok 6 - Matching: test_gjson_transform_nested_and: matches: Expected {"nested":"cow"} to have key matching "nope" the transform chain was [{"gjson":{"Path":"moo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +not ok 7 - Matching: test_gjson_transform_nested_count: matches: Expected 15 to be numerically le 10 the transform chain was [{"gjson":{"Path":"count"}},{"to-numeric":{}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +not ok 8 - Matching: test_gjson_transform_nested_prefix: matches: Expected "bar" to have prefix "x" the transform chain was [{"gjson":{"Path":"foo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +not ok 9 - Matching: test_gjson_transform_nested_this: matches: Expected {"count":"15","foo":"bar","moo":{"nested":"cow"}} to have key matching "nope" the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +not ok 10 - Matching: test_gjson_transform_not_key: matches: Expected {"nested":"cow"} not to have key matching "nested" the transform chain was [{"gjson":{"Path":"moo"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +not ok 11 - Matching: test_gjson_transform_simple: matches: Expected "cow" to equal "cowx" the transform chain was [{"gjson":{"Path":"moo.nested"}}] the raw value was "{\"foo\": \"bar\", \"moo\": {\"nested\": \"cow\"}, \"count\": \"15\"}" +not ok 12 - Matching: test_gjson_using_this_and_equal: matches: Expected {"baz":"bing","foo":"bar"} to equal {"baz":"bing","fox":"bar"} the transform chain was [{"gjson":{"Path":"@this"}}] the raw value was "{\"foo\": \"bar\", \"baz\": \"bing\"}" +not ok 13 - Matching: test_numeric_string: matches: Expected "128" to equal "129" the transform chain was [{"to-string":{}}] the raw value was 128 +not ok 14 - Matching: test_reader_as_single_string: matches: Expected "cool" to equal "not-cool" +not ok 15 - Matching: test_reader_using_int_matchers: matches: Expected 40 to be numerically le 20 the transform chain was [{"to-numeric":{}}] the raw value was "40" +not ok 16 - Matching: test_reader_using_string_matchers: matches: Expected "foo bar\n15\nmoo cow\n" to have length 15 +not ok 17 - Matching: test_string_float: matches: Expected 128.3 to be numerically eq 129.3 the transform chain was [{"to-numeric":{}}] the raw value was "128.3" +not ok 18 - Matching: test_string_numeric: matches: Expected 128 to be numerically eq 129 the transform chain was [{"to-numeric":{}}] the raw value was "128" diff --git a/testdata/out_matching_transformers_failing.2.nagios b/testdata/out_matching_transformers_failing.2.nagios new file mode 100644 index 00000000..3e7a6bbf --- /dev/null +++ b/testdata/out_matching_transformers_failing.2.nagios @@ -0,0 +1 @@ +GOSS CRITICAL - Count: 18, Failed: 18, Skipped: 0, Duration: diff --git a/util/config.go b/util/config.go index 9185bd47..81751aed 100644 --- a/util/config.go +++ b/util/config.go @@ -43,6 +43,7 @@ type Config struct { Proxy string RequestHeader []string RetryTimeout time.Duration + RunLevel string Server string Sleep time.Duration Spec string diff --git a/validate.go b/validate.go index a967d938..9e4c394c 100644 --- a/validate.go +++ b/validate.go @@ -10,6 +10,7 @@ import ( "time" "github.com/fatih/color" + "github.com/onsi/gomega/format" "github.com/goss-org/goss/outputs" "github.com/goss-org/goss/resource" @@ -98,18 +99,26 @@ func ValidateResults(c *util.Config) (results <-chan []resource.TestResult, err // and supports retries and more, this is the full featured Validate used // by the typical CLI invocation and will produce output to StdOut. Use // ValidateResults for programmatic access -func Validate(c *util.Config, startTime time.Time) (code int, err error) { +func Validate(c *util.Config) (code int, err error) { err = setLogLevel(c) if err != nil { return 1, err } - outputConfig := util.OutputConfig{ - FormatOptions: c.FormatOptions, - } - gossConfig, err := getGossConfig(c.Vars, c.VarsInline, c.Spec) if err != nil { - return 1, err + return 78, err + } + return ValidateConfig(c, gossConfig) +} + +func ValidateConfig(c *util.Config, gossConfig *GossConfig) (code int, err error) { + // Needed for contains-elements + // Maybe we don't use this and use custom + // contain_element_matcher is needed because it's single entry to avoid + // transform message + format.UseStringerRepresentation = true + outputConfig := util.OutputConfig{ + FormatOptions: c.FormatOptions, } sys := system.New(c.PackageManager) @@ -127,10 +136,10 @@ func Validate(c *util.Config, startTime time.Time) (code int, err error) { sleep := c.Sleep retryTimeout := c.RetryTimeout i := 1 + startTime := time.Now() for { - iStartTime := time.Now() out := validate(sys, *gossConfig, c.DisabledResourceTypes, c.MaxConcurrent) - exitCode := outputer.Output(ofh, out, iStartTime, outputConfig) + exitCode := outputer.Output(ofh, out, outputConfig) if retryTimeout == 0 || exitCode == 0 { return exitCode, nil }