Skip to content

The gossfile configuration

Syver test creation

Syver tests can be created by using either of following methods.

  1. syver autoadd <resource to test>
  2. syver add <resource to test>
  3. manually create YAML/JSON test file by hand.

To customize the parameters generated by syver add and syver autoadd YAML file you need to manually edit it.

syver add package nginx will generate below YAML

package:
  nginx:
    installed: true
    versions:

    - 1.17.8

To test uninstall scenario you would need to manually edit it and set it as below.

package:
  nginx:
    installed: false

Important note about goss file format

It is important to note that both YAML and JSON are formats that describe a nested data structure.

WRONG way to write a goss file
file:
  /etc/httpd/conf/httpd.conf:
    exists: true

service:
  httpd:
    enabled: true
    running: true

file:
  /var/www/html:
    filetype: directory
    exists: true

If you try to validate this file, it will only run the second file test:

$ syver validate --format documentation
File: /var/www/html: exists: matches expectation: [true]
File: /var/www/html: filetype: matches expectation: ["directory"]
Service: httpd: enabled: matches expectation: [true]
Service: httpd: running: matches expectation: [true]

Total Duration: 0.014s
Count: 8, Failed: 0, Skipped: 0

As you can see, the first file check has not been run because the second file entry overwrites the previous one.

You need to make sure all the entries of the same type are under the same declaration.

This is the CORRECT way to write a goss file
file:
  /etc/httpd/conf/httpd.conf:
    exists: true
  /var/www/html:
    filetype: directory
    exists: true

service:
  httpd:
    enabled: true
    running: true

Running validate with this configuration will correctly check both files:

$ syver validate --format documentation
File: /var/www/html: exists: matches expectation: [true]
File: /var/www/html: filetype: matches expectation: ["directory"]
File: /etc/httpd/conf/httpd.conf: exists: matches expectation: [true]
Service: httpd: enabled: matches expectation: [true]
Service: httpd: running: matches expectation: [true]

Total Duration: 0.014s
Count: 10, Failed: 0, Skipped: 0

Please note that using the syver add and syver autoadd command will create a valid file, but if you're writing your files by hand you'll save a lot of time by taking this in consideration.

If you want to keep your tests in separate files, the best way to obtain a single, valid, file is to create a main goss file that includes the others with the gossfile directive and then render it.

Schema

A Json draft 7 schema available at https://raw.githubusercontent.com/krameff/syver/main/docs/schema.yaml makes it easier to edit simple goss.yaml files in IDEs, providing usual coding assistance such as inline documentation, completion and static analysis. See #793 for screenshots.

For example, to configure the Json schema in JetBrains intellij IDEA, follow documented instructions, with arguments such as:

  • schema url=https://raw.githubusercontent.com/krameff/syver/main/docs/schema.yaml
  • schema version=Json schema version 7
  • file path pattern=*/goss.yaml

Unknown top-level keys

A "top-level key" is one of the un-indented keys at the very start of a gossfile -- file:, port:, command:, gossfile: and so on, the ones listed in Available tests below plus a handful of others like discovery:. If you write one of these with a typo, or one that a future syver understands but the version you're running does not, that whole block is not rejected. It is skipped, silently, and syver runs the rest of the file as if it had never been written -- same exit code, same Count/Failed, nothing to tell you a block went untested.

A spec that silently skips a block you thought was being checked is worse than one that fails outright: nobody investigates a green result. Syver still skips the key -- nothing about what gets tested changes -- but it now says so:

$ syver -g syver.yaml validate
[WARN] syver.yaml:4: unknown top-level key "some-vendor-thing" -- ignored
[WARN] syver.yaml:7: unknown top-level key "prot" -- ignored (did you mean "port"?)

If the key is close to a real one, the warning suggests the fix. The warning is informational only -- it does not fail the run or change Count/Failed in the result.

Two kinds of top-level key are deliberately exempt, and neither produces a warning:

  • A key prefixed x-. This is the same convention docker-compose uses for extension fields: a documented, explicit way to say "this block is mine, leave it alone."
  • A key whose value carries a YAML anchor, such as a shared block referenced elsewhere with <<::
common-checks: &common
  exit-status: 0

command:
  echo one:
    <<: *common
  echo two:
    <<: *common

common-checks: is not a key syver understands on its own, but it exists only to carry the anchor command: merges in below, so it is not warned about. This is detected structurally (does the value define an anchor?), not by name -- a block with no anchor is not covered by this exemption, which is exactly what the x- prefix is for.

This check only applies to YAML gossfiles. JSON gossfiles have the same gap -- an unknown JSON top-level key is dropped the same way -- and it is not yet covered.

A --vars file is never checked against this. A vars file is arbitrary user-supplied data with no fixed vocabulary, so every key in it is expected to be "unknown" to syver and none of them warn.

See examples/unknown-top-level-key.yaml for a runnable spec that shows both sides of this: a genuine typo that warns, and a legitimate anchor-carrying block next to it that does not.

Available tests

addr

Validates if a remote address:port are accessible.

addr:
  tcp://ip-address-or-domain-name:80:
    # required attributes
    reachable: true
    # 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. This can be used in combination with the gjson matcher to create powerful syver custom tests.

command:
  'go version':
    # required attributes
    exit-status: 0
    # optional attributes
    # defaults to hash key
    exec: "go version"
    stdout:

    - go version go1.6 linux/amd64
    stderr: []
    timeout: 10000 # in milliseconds
    skip: false

stdout and stderr can be a string or pattern

The exec attribute is the command to run; this defaults to the name of the hash for backwards compatibility

A gossfile is executable input

exec is run through the system shell (sh -c on Linux and macOS, cmd /c on Windows), so shell metacharacters, pipes and redirection all work -- and anyone who can supply or edit a gossfile can run arbitrary commands as the user running syver. Treat a gossfile with the same care as a shell script: only run ones you trust, and remember that a --vars file templated into an exec string carries the same power, since whoever controls the vars file controls part of the command.

On timeout the command is killed along with any child processes it started, on every platform.

One exception on Linux and macOS, deliberate rather than an oversight: a process that detaches itself into a new session -- setsid, nohup, most daemons -- has left the group syver signals, so it survives the timeout and is reparented to init. Syver has no portable way to find it again. Under serve each such timeout leaves one process behind for as long as it chooses to run, so a spec that repeatedly times out a daemonising command will accumulate them. If a check needs to start a daemon, have it start the daemon and exit, rather than relying on the timeout to clean up.

Windows has no such exception. The process tree is held in a Job Object, which a process cannot leave unless it was created with CREATE_BREAKAWAY_FROM_JOB and the job allows it, so a detaching child is terminated with the rest. A command that succeeds is never touched: only a timeout terminates the tree, so a check that deliberately starts a background process and exits zero leaves it running.

timeout values

timeout is in milliseconds and defaults to 10000 when omitted or set to 0. A negative value is a mistake rather than a way to disable the bound: syver warns once and uses the default. There is no way to run a command unbounded.

A command whose child processes outlive it can take up to twice the timeout to return, because syver waits that long for their output streams to close before reading the result. Commands that exit normally return immediately.

timeout applies only to command. The checks syver runs on your behalf for other resource types -- systemctl, service, rpm, dpkg-query, apk, pacman, getent -- carry their own fixed bound of 30 seconds, which is not configurable, because how long they take is a property of the host's tooling rather than of your spec. On top of that syver allows a few more seconds to collect whatever output is still arriving, so one of these takes at most about 35 seconds to give up on a host whose package or service manager has stopped answering. Checks run in parallel, so several stuck ones mostly overlap rather than queue -- though if more are stuck at once than syver runs in parallel, the ones still waiting for a slot do add to the total.

dns

Validates that the provided address is resolvable and the addrs it resolves to.

dns:
  localhost:
    # required attributes
    resolvable: true
    # optional attributes
    # defaults to hash key
    resolve: localhost
    addrs:

    - 127.0.0.1
    - ::1
    server: 8.8.8.8 # Also supports server:port
    timeout: 500 # in milliseconds (Only used when server attribute is provided)

It is possible to validate the following types of DNS records, but requires the server attribute be set:

  • A
  • AAAA
  • CAA
  • CNAME
  • MX
  • NS
  • PTR
  • SRV
  • TXT

To validate specific DNS address types, prepend the hostname with the type and a colon, a few examples:

dns:
  # Validate a CNAME record
  CNAME:c.dnstest.io:
    resolvable: true
    server: 208.67.222.222
    addrs:

    - "a.dnstest.io."

  # Validate a PTR record
  PTR:8.8.8.8:
    resolvable: true
    server: 8.8.8.8
    addrs:

    - "dns.google."

  # Validate and SRV record
  SRV:_https._tcp.dnstest.io:
    resolvable: true
    server: 208.67.222.222
    addrs:

    - "0 5 443 a.dnstest.io."
    - "10 10 443 b.dnstest.io."

Please note that if you want localhost to only resolve 127.0.0.1 you'll need to use Advanced Matchers

dns:
  localhost:
    resolvable: true
    addrs:
      consist-of: [127.0.0.1]
    timeout: 500 # in milliseconds

file

Validates the state of a file, directory, socket, or symbolic link

file:
  /etc/passwd:
    # 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, socket
    contents: [] # Check file content for these patterns
    md5: 7c9bb14b3bf178e82c00c2a4398c93cd # md5 checksum of file
    # A stronger checksum alternatives to md5 (recommended)
    sha256: 7f78ce27859049f725936f7b52c6e25d774012947d915e7b394402cfceb70c4c
    sha512: cb71b1940dc879a3688bd502846bff6316dd537bbe917484964fe0f098e9245d80958258dc3bd6297bf42d5bd978cbe2c03d077d4ed45b2b1ed9cd831ceb1bd0
  /etc/alternatives/mta:
    # required attributes
    exists: true
    # optional attributes
    filetype: symlink # file, symlink, directory, socket
    linked-to: /usr/sbin/sendmail.sendmail
    skip: false

contents can be a string or a pattern

acl and acl-sid (Windows only)

acl reports the file's DACL, one list element per ACE, spelled the way icacls prints it -- so a failing assertion can be compared against icacls <path> with no translation step:

file:
  C:\Windows\System32\drivers\etc\hosts:
    exists: true
    # every ACE must be present; others may exist
    acl:

    - 'NT AUTHORITY\SYSTEM:(I)(F)'
    - 'BUILTIN\Administrators:(I)(F)'
    - 'BUILTIN\Users:(I)(RX)'
  C:\ProgramData\app\secrets.dat:
    exists: true
    # these ACEs and NO others -- the form a hardening control wants
    acl:
      consist-of:
      - 'NT AUTHORITY\SYSTEM:(F)'
      - 'BUILTIN\Administrators:(F)'
    # and nothing may be inherited from the parent
    acl-sid:
      not:
        contain-element:
          contain-substring: '(I)'

An element is PRINCIPAL:(I)(DENY)(OI)(CI)(IO)(NP)(RIGHTS), with each parenthesised group present only when it applies, in that order. (I) marks an inherited ACE, (DENY) a deny ACE, the rest are inheritance propagation flags, and RIGHTS is a simple-rights letter (F, M, RX, R, W, D, N) or a comma-joined list of specific tokens (GR,GE, RD,WD,X, ...), exactly as icacls /? documents them.

Because it is an ordinary list, every array matcher applies: a bare list means "at least these", consist-of means "these and no others", and contain-element with a string matcher tests one ACE. The same principal can appear more than once, which is normal rather than exotic -- on C:\Windows, BUILTIN\Administrators holds (M) on the directory itself and (OI)(CI)(IO)(F) for the things inside it.

acl-sid is the same ACEs in the same order with principals as SID strings. Prefer it for any spec that must run on more than one Windows locale: BUILTIN\Administrators is a localised name and a spec written on English Windows will not match a German host, whereas S-1-5-32-544 is the same everywhere. syver add writes acl, because it exists to give you something readable to start from; switching the key is a deliberate edit.

Both attributes report the DACL as it stands. Neither computes effective access for a principal, which would fold in group membership and deny precedence and could no longer be compared against icacls. Audit entries (the SACL) are not read at all: that needs a privilege beyond what reading permissions requires.

On any platform other than Windows both attributes error rather than reporting an empty list, so a cross-platform gossfile fails loudly instead of quietly agreeing with itself.

gossfile

Import other gossfiles from this one. This is the best way to maintain a large number of tests, and/or create profiles. See render for more examples. Glob patterns can be also be used to specify matching gossfiles.

gossfile:
  myapplication:
    file: myapp_gossfile.yaml
    skip: false
  *.yaml:
    skip: true
  goss_httpd.yaml: {}
  /etc/goss.d/*.yaml: {}

You can specify the gossfile(s) either as the resource key, or using the 'file' attribute.

If the 'skip' attribute is true, then the file is not processed. If the filename is a glob pattern, then none of the matching files are processed. Note that this is not the same as skipping the contained resources; any overrides in the referenced gossfile will not be processed, and the resource count will not be incremented. Skipping a gossfile include is the same as omitting the gossfile resource entirely.

group

Validates the state of a group

group:
  nfsnobody:
    # required attributes
    exists: true
    # optional attributes
    # defaults to hash key
    groupname: /etc/passwd
    gid: 65534
    skip: false

http

Validates HTTP response status code and content.

http:
  https://www.google.com:
    # 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
    request-headers: # Set request header values

       - "Content-Type: text/html"
    request-query-params: # Set URL query parameters (properly encoded; supports duplicate keys)
      foo:
        - bar
    headers: [] # Check http response headers for these patterns (e.g. "Content-Type: text/html")
    request-body: '{"key": "value"}' # request body
    body: [] # Check http response content for these patterns
    username: ""  # username for basic auth
    password: ""  # password for basic auth
    ca-file: ""   # CA root certs pem file, ex: /etc/ssl/cert.pem
    cert-file: "" # certificate file to use for authentication (used with key-file)
    key-file: ""  # private-key file to use for authentication (used with cert-file)
    proxy: "" # proxy server to proxy traffic through. Proxy can also be set with environment variables http_proxy.
    skip: false
    method: PUT # http method

Note

only the first Host header will be used to set the Request.Host value if multiple are provided.

interface

Validates network interface values

interface:
  eth0:
    # required attributes
    exists: true
    # optional attributes
    # defaults to hash key
    name: eth0
    addrs:

    - 172.17.0.2/16
    - fe80::42:acff:fe11:2/64
    mtu: 1500

kernel-param

Validates kernel param (sysctl) value.

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.

mount

Validates mount point attributes.

mount:
  /home:
    # required attributes
    exists: true
    # optional attributes
    # defaults to hash key
    timeout: 1000
    mountpoint: /home
    opts:

    - rw
    - relatime
    # This maps to the per-superblock options, see:
    # https://man7.org/linux/man-pages/man5/proc.5.html
    # https://man7.org/linux/man-pages/man2/mount.2.html
    vfs-opts:
    - rw
    source: /dev/mapper/fedora-home
    filesystem: xfs
    usage: #% of blocks used in this mountpoint
      lt: 95

matching

Validates specified content against a matcher. Best used with Templates.

With Templates

Let's say we have a data.json file that gets generated as part of some testing pipeline:

{
  "instance_count": 14,
  "failures": 3,
  "status": "FAIL"
}

This could then be passed into syver: syver --vars data.json validate

And then validated against:

matching:
  check_instance_count: # Make sure there is at least one instance
    content: {{ .Vars.instance_count }}
    matches:
      gt: 0

  check_failure_count_from_all_instance: # expect no failures
    content: {{ .Vars.failures }}
    matches: 0

  check_status:
    content: {{ .Vars.status }}
    matches:

      - not: FAIL

Without Templates

matching:
  has_substr: # friendly test name
    content: some string
    matches:
      match-regexp: some str
  has_2:
    content:

      - 2
    matches:
      contain-element: 2
  has_foo_bar_and_baz:
    content:
      foo: bar
      baz: bing
    matches:
      and:
        - have-key: baz

registry

Validates a Windows registry key or value. Windows only -- on Linux and macOS this resource is not available, see platforms.

Uses the native golang.org/x/sys/windows/registry API, so no PowerShell or reg.exe subprocess is involved.

registry:
  # a key: the trailing backslash is what makes this a key check rather
  # than a check on a value named CurrentVersion
  HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\:
    exists: true

  # a value: the last path segment is read as the value name
  HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentVersion:
    exists: true
    value: "10.0"
    type: REG_SZ

  # use :: when the value name itself contains a backslash, or when the
  # split would otherwise be ambiguous
  HKLM\SOFTWARE\Vendor\Product::Install\Path:
    exists: true

Supported hives, as the first path segment. Each may be written short, long, or with the PowerShell provider colon, so a path pasted from regedit's address bar or from Get-ItemProperty output works unedited:

Short Long PowerShell
HKLM HKEY_LOCAL_MACHINE HKLM:
HKCU HKEY_CURRENT_USER HKCU:
HKCR HKEY_CLASSES_ROOT HKCR:
HKU HKEY_USERS HKU:
HKCC HKEY_CURRENT_CONFIG HKCC:

Hive names are case-insensitive.

Attributes:

  • exists -- whether the key or value is present. The only required attribute.
  • value -- the data held by the value, always read as a string. Accepts any matcher. Note REG_MULTI_SZ is returned as its entries joined by newlines, not as a list, so use contain-substring or match-regexp rather than list matchers like contain-element.
  • type -- the value's data type. Every REG_* type is reported, including REG_NONE, REG_LINK, REG_DWORD_BIG_ENDIAN and the three REG_RESOURCE_* hardware descriptor types.
  • view -- which WOW64 registry view to read: 32, 64, or native. Defaults to native, which is what every spec written before this attribute existed gets, and means whatever the OS shows a 64-bit process.

Choosing a view

On 64-bit Windows some keys exist twice. A 32-bit program is redirected under Wow6432Node and sees a different copy from a 64-bit one. view: lets a spec say which copy it means instead of inheriting syver's own architecture:

registry:
  # the redirected copy a 32-bit installer wrote, under Wow6432Node
  HKLM\SOFTWARE\Vendor\Product\Version:
    exists: true
    view: "32"
  # the native 64-bit copy. Same path, different branch -- so give the two
  # entries distinct keys, or use `name:` to point both at one path
  HKLM\SOFTWARE\Vendor\Product64\Version:
    exists: true
    view: "64"

Quote the value. Syver itself accepts view: 32 unquoted -- the YAML integer is coerced to a string on load, and renders back out as view: "32" -- but schema.yaml types the attribute as a string, so the unquoted form fails schema validation against a spec syver would have run. Quoting keeps the two in step. view is matched case-insensitively and surrounding whitespace is ignored, so NATIVE and native are the same value.

The trailing backslash, and why it is not guessed

By default the last backslash-separated segment of the path is treated as the value name. Use the explicit :: separator when that guess would be wrong, for example when a value name contains backslashes.

A trailing backslash addresses the key itself rather than a value, and the difference is not cosmetic. These two ask different questions and both answer truthfully:

HKLM\...\ProfileList     exists -> false   is there a VALUE named ProfileList?
HKLM\...\ProfileList\    exists -> true    is there a KEY named ProfileList?

syver does not guess which you meant, because guessing would move the ambiguity somewhere you cannot see it. Instead, when a value lookup misses and a key of that name exists in the same place, it logs a warning naming the alternative. Asserting exists: false against the first form therefore passes and tells you it may not be testing what you think.

A key can exist while having no default value, so exists: true on a path ending in a backslash alongside a value lookup that reports not-found is a coherent pair rather than a contradiction.

REG_EXPAND_SZ is compared unexpanded

A REG_EXPAND_SZ value is matched as stored. A value holding %SystemRoot%\System32 is compared as that literal text, not as C:\Windows\System32. This is deliberate: benchmarks state the stored form, and expansion depends on the environment of whoever is asking.

package

Validates the state of a package

package:
  httpd:
    # required attributes
    installed: true
    # optional attributes
    # defaults to hash key
    name: httpd
    versions:

    - 2.2.15
    skip: false

Note

this check uses the --package <format> parameter passed on the command line.

port

Validates the state of a local port.

Note

Syver might consider your port to be listening on tcp6 rather than tcp, try running syver add port .. to see how syver detects it. (explanation)

port:
  # {tcp,tcp6,udp,udp6}:port_num
  tcp:22:
    # 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
    pid: # what PID(s) own the socket
    - 1234
    skip: false

Note

pid is only populated when the OS lets syver map the socket back to its owning process (this needs the same access as reading /proc/<pid>/fd on Linux). If the owner can't be resolved, that connection is left out of pid rather than reported as PID 0.

Note

syver add/discovery does not auto-populate pid, unlike the other attributes on this page -- PIDs get reassigned on every restart, so a value captured at add time would already be stale by the time the gossfile runs. Add pid by hand if you want it checked.

process

Validates if a process is running.

process:
  chrome:
    # required attributes
    running: true
    # optional attributes
    # defaults to hash key
    comm: chrome
    status: # process state(s), e.g. detect zombie processes

    - running
    user: # user(s) the process is running as, e.g. flag anything running as root
    - chrome
    skip: false

Note

status and user are aggregated across every PID matching this executable, so a chrome entry backed by several processes reports the distinct set of states/owners seen, not just one of them. status values are one of running, sleep, stop, idle, wait, lock, zombie.

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/<PID>/comm.

service

Validates the state of a service.

service:
  sshd:
    # 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

Windows-only service attributes

On Windows, service: reads the Service Control Manager directly and six further attributes are available:

service:
  EventLog:
    enabled: true
    running: true
    start-type: automatic     # boot, system, automatic, manual, disabled
    delayed-start: false
    run-as: 'NT AUTHORITY\LocalService'
    display-name: Windows Event Log
    pid:
      gt: 0                   # a range, not a value -- see below
  Dnscache:
    dependencies:
      contain-element: nsi
  • start-type is what enabled is derived from. enabled is true for boot, system and automatic.
  • delayed-start is a separate flag rather than a sixth start type: the Services UI's "Automatic (Delayed Start)" is start-type: automatic plus delayed-start: true.
  • run-as is the account as the SCM stores it, not normalised. Windows itself reports LocalSystem, localSystem and NT AUTHORITY\LocalService in different places, and syver reports what the machine says.
  • dependencies lists services and load-order groups that must start first, verbatim. A name beginning with + is a load-order group, not a service.
  • display-name is localised. Assert the service key name instead in a spec that has to run on more than one Windows language.
  • pid is 0 when the service is not running, and syver add service does not emit it: a pid changes at every restart, so a generated spec pinning one would fail at the next reboot. Assert a range.

All six error on every other platform, so a spec that sets one on Linux fails rather than quietly comparing against an empty value.

Note

This will not automatically check if the process is alive, it will check the status from systemd/upstart/init.

user

Validates the state of a user

user:
  nfsnobody:
    # required attributes
    exists: true
    # optional attributes
    # defaults to hash key
    username: nfsnobody
    uid: 65534
    gid: 65534
    groups:

    - nfsnobody
    home: /var/lib/nfs
    shell: /sbin/nologin
    skip: false

Note

This check is inspecting the contents of local passwd file /etc/passwd, this does not validate remote users (e.g. LDAP).

Matchers

Default Matchers

Default matchers are determined by the attribute value received from the system.

An empty list is not an expectation

stderr: [] does not assert that stderr was empty. An empty list means "no expectation for this attribute", and the attribute is skipped entirely -- it produces no result and does not appear in the test count.

This is the form syver add and syver autoadd emit for an attribute they found nothing to assert about, which is why it appears throughout generated gossfiles and in the examples on this page. It behaves the same way on every attribute of every resource type.

To assert that output really is empty, say so:

command:
  quiet-command:
    exec: /usr/local/bin/quiet
    exit-status: 0
    stderr:
      have-len: 0

The one exception is each resource's mandatory attribute -- file.exists, command.exit-status, http.status and so on. Those always report, so a resource always produces at least one result.

Bool, Strings, Integers

Bool, Strings and integers are compared using equality, for example:

matching:
  basic_string:
    content: 'foo'
    matches: 'foo'

user:
  nfsnobody:
    exists: true
    uid: 65534

Arrays

Arrays are treated as a contains-elements by default, this validates that the expected test is a subset of the returned system state.

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

This is the most magical matcher for syver. 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 syver to validate large files/content efficiently.

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 in the Go regexp/syntax package

Warning

You will need the double backslash (\\) escape for Regex special entities, for example \\s for blank spaces.

Example

file:
/tmp/test.txt:
    exists: true
    contents:

    - "foo"
    - "!bar"
    - "/[Gg]oss/"

The above can be expressed as:

file:
/tmp/test.txt:
    exists: true
    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, syver will attempt to transform the system state type before matching it.

For example, kernel-param attribute returns a string, however, it can be tested using numeric comparisons:

kernel-param test

kernel-param:
net.core.somaxconn:
    value: "128"

(failing) kernel-param test with transform

kernel-param:
net.core.somaxconn:
    value: {gt: 200}

When a transformed test fails, it will detail the transformers used, the -o exclude_raw option can be used to exclude the raw, untransformed attribute value:

$ syver v
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: 1, Failed: 1, Skipped: 0

$ syver v -o exclude_raw
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

Advanced Matchers

Syver supports advanced matchers by converting YAML input to gomega matchers.

An empty matcher group is a syntax error

A matcher group with nothing in it asserts nothing, so Syver rejects it rather than report a result without looking at the value:

and: []               # syntax error
or: []                # syntax error
contain-elements: []  # syntax error
gjson: {}             # syntax error

Two empty forms are still accepted, because each checks the value: consist-of: [] asserts it is empty, and a bare [] (as in stderr: [], which syver add writes) is unchanged.

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"

  • '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

matching:
example:
    content: 42
    matches:
    and:

        - '42'
        - have-prefix: '4'
        - have-suffix: '2'
        - match-regexp: '\d{2}'
        - contain-substring: '2'

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

matching:
example:
    content: "42"
    matches:
    and:

        - 42
        - 42.0
        - gt: 40
        - lt: 45

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

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

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

Note

When system returns a string it is converted into a one element array and matched

semver-constraint

Checks that all versions match semver constraint or range syntax. This uses semver under the hood, however, wildcards (e.g. 1.X are not officially supported and may go away in a future release).

Example

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 passes the matcher

Example:

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"}}

Discovery

Discovery tests run lightweight checks and expose the results as template variables for conditional test suites. This supports portable gossfiles across heterogeneous estates (see goss-org/goss#784).

Define discovery tests under the top-level discovery key. Each test requires a register attribute naming the variable written to output:

discovery:
  package:
    auditd:
      register: auditd_installed
      installed: true
  file:
    /usr/bin/clang:
      register: clang_available
      exists: true

Run discovery and emit vars-compatible JSON:

syver validate -g discovery.yaml --format discovery

Output shape:

{
  "Discovered": {
    "auditd_installed": true,
    "clang_available": false
  }
}

Run discovery before the main gossfile in one command (preferred):

syver validate -g goss.yml --discover discovery.yaml

Or put discovery tests in the same gossfile as the main suite:

discovery:
  file:
    /etc/hosts:
      register: hosts_exists
      exists: true

{{ if .Discovered.hosts_exists }}
file:
  /etc/hosts:
    exists: true
{{ end }}
syver validate -g goss-inline.yml

When both --discover and an inline discovery: section are present, the --discover file wins and inline discovery is ignored (with an INFO log).

Pipe discovery output into a main gossfile (still supported for tooling and CI export):

syver --vars <(syver validate -g discovery.yaml --format discovery) validate -g goss.yml

Use verbose documentation output on the main run:

syver --vars <(syver validate -g discovery.yaml --format discovery) \
     validate -g goss.yml --format documentation

Or save discovery output to a file first:

syver validate -g discovery.yaml --format discovery > discovered.json
syver --vars discovered.json validate -g goss.yml --format documentation

Complete example (also in integration-tests/syver/examples/discovery/):

discovery.yaml

discovery:
  file:
    /etc/hosts:
      register: hosts_exists
      exists: true

goss.yml

{{ if .Discovered.hosts_exists }}
file:
  /etc/hosts:
    exists: true
    contents:

      - localhost
{{ end }}
syver validate -g goss.yml --discover discovery.yaml --format documentation

Use .Discovered in templates:

{{ if .Discovered.auditd_installed }}
file:
  /etc/audit/auditd.conf:
    exists: true
{{ end }}

Discovery checks always exit 0 when execution succeeds; boolean values reflect actual system state. The main validate run decides pass/fail.

Combine discovery with test dependencies: discovery populates .Discovered so templates can gate which tests exist; depends-on then skips dependents when a prerequisite fails.

{{ if .Discovered.hosts_exists }}
file:
  prereq:
    path: /nonexistent/goss-discover-dep-base
    exists: true

command:
  dependent:
    depends-on:

      - prereq
    exec: true
    exit-status: 0
{{ end }}
syver validate -g goss-with-deps.yml --discover discovery.yaml

See integration-tests/syver/examples/discovery/goss-with-deps.yml.

Test dependencies

Tests may declare prerequisites with depends-on. Dependent tests are skipped (not failed) when a prerequisite fails (see goss-org/goss#1043).

file:
  {{.Vars.root_dir}}/bin/clang:
    exists: true

command:
  clang-compile-basic:
    depends-on:

      - {{.Vars.root_dir}}/bin/clang
    exec: 'clang basic.c -o basic'
    exit-status: 0

References use the gossfile map key, or type:key when ambiguous across resource types. Independent dependency chains still run in parallel; only declared dependencies are serialized.

Templates

Syver test files can leverage golang's text/template to allow for dynamic or conditional tests.

Available variables:

  • {{.Env}} - Containing environment variables
  • {{.Vars}} - Containing the values defined in --vars files (merged in order)
  • {{.Discovered}} - Values from discovery (--discover, inline discovery:, or --vars)

Available functions:

Templates can generate secrets and certificates

The template function library includes cryptographic helpers, and they have always been available rather than being new: bcrypt, htpasswd, derivePassword, genPrivateKey, genCA, genCAWithKey, genSelfSignedCert, genSelfSignedCertWithKey, genSignedCert, genSignedCertWithKey, buildCustomCert, encryptAES and decryptAES.

They return values into the rendered gossfile and write nothing to disk. If you are reviewing a gossfile someone else wrote, this is worth knowing: a template can mint a certificate or encrypt a string before any check runs. Note that a gossfile can already run arbitrary commands through the command: resource, so these functions do not widen what an author can do, they are simply easy to miss.

  • Custom functions:

    mkSlice "ARG1" "ARG2"
    Returns a slice of all the arguments. See examples below for usage.
    getEnv "var" ["default"]
    A more forgiving env var lookup. If key is missing either "" or default (if provided) is returned.
    readFile "fileName"
    Reads file content into a string, trims whitespace. Useful when a file contains a token.

    Note

    Goss will error out during the parsing phase if the file does not exist, no tests will be executed.
    regexMatch "(some)?reg[eE]xp"
    Tests the piped input against the regular expression argument.
    toLower
    Changes piped input to lowercase
    toUpper
    Changes piped input to UPPERCASE
    findStringSubmatch regex string
    Returns map[string]interface{} with the names of the parenthesized subexpressions, like (?P<first>[a-z])
    {{ $regexDBrc := "\\'mysql:\\/\\/(?P<login>[a-z0-9]+):(?P<password>[a-z0-9]+)@localhost\\/(?P<database>roundcube_[a-z0-9]+)\\';"}}
    
    {{ $rcConf := readFile /home/user/roundcube/config.inc.php | findStringSubmatch $regexDBrc }}
    {{ $UserDBrc := $rcConf | get "login" }}
    {{ $PassDBrc  := $rcConf | get "password" }}
    {{ $DBrc := $rcConf | get "database" }}
    

    If not exists named parenthesized subexps, returns stringfied array string:

    {{ $regexDBrc := "\\'mysql:\\/\\/([a-z0-9]+):([a-z0-9]+)@localhost\\/(roundcube_[a-z0-9]+)\\';"}}
    
    {{ $rcConf := readFile /home/user/roundcube/config.inc.php | findStringSubmatch $regexDBrc }}
    {{ $UserDBrc := $rcConf | get "1" }}
    {{ $PassDBrc  := $rcConf | get "2" }}
    {{ $DBrc := $rcConf | get "3" }}
    

    NOTE: stringfied string array begins with "1" ("0" is all the string matched)

Warning

gossfiles containing text/template {{}} controls will no longer work with syver add/autoadd. One way to get around this is to split your template and static goss files and use gossfile to import.

Note

Three of the custom functions share a name with a Sprout function, and the custom one takes priority: toUpper, toLower and regexMatch.

For toUpper and toLower this is not merely cosmetic. Sprout's versions accept any type and return nothing when given a non-string, so {{ toUpper 42 }} would render as an empty value. The custom versions take a string and nothing else, so the same expression fails the run with a template error naming the line. A check that silently renders nothing is worse than one that stops, so the stricter behaviour is deliberate.

Examples

Using puppetlabs/facter or chef/ohai as external tools to provide vars.

syver --vars <(ohai) validate
syver --vars <(facter -j) validate

Using mkSlice to define a loop locally.

file:
{{- range mkSlice "/etc/passwd" "/etc/group"}}
  {{.}}:
    exists: true
    mode: "0644"
    owner: root
    group: root
    filetype: file
{{end}}

Using upper function from Sprout.

matching:
  sping_basic:
    content: {{ "hello!" | upper | repeat 5 }}
    matches:
      match-regexp: "HELLO!HELLO!HELLO!HELLO!HELLO!"

Using Env variables and a vars file:

vars.yaml
centos:
  packages:
    kernel:

      - "4.9.11-centos"
      - "4.9.11-centos2"
debian:
  packages:
    kernel:
      - "4.9.11-debian"
      - "4.9.11-debian2"
users:
  - user1
  - user2
goss.yaml
package:
# Looping over a variables defined in a vars.yaml using $OS environment variable as a lookup key
{{range $name, $vers := index .Vars .Env.OS "packages"}}
  {{$name}}:
    installed: true
    versions:
    {{range $vers}}

      - {{.}}
    {{end}}
{{end}}

# This test is only when the OS environment variable matches the pattern
{{if .Env.OS | regexMatch "[Cc]ent(OS|os)"}}
  libselinux:
    installed: true
{{end}}

# Loop over users
user:
{{range .Vars.users}}
  {{.}}:
    exists: true
    groups:

    - {{.}}
    home: /home/{{.}}
    shell: /bin/bash
{{end}}


package:
{{if eq .Env.OS "centos"}}
  # This test is only when $OS environment variable is set to "centos"
  libselinux:
    installed: true
{{end}}
Rendered results
# To validate:
$ OS=centos syver --vars vars.yaml validate
# To render:
$ OS=centos syver --vars vars.yaml render
# To render with debugging enabled:
$ OS=centos syver --vars vars.yaml render --debug