Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,80 @@ $ goss serve --format json &
$ curl -H "Accept: application/vnd.goss-rspecish" localhost:8080/healthz
```

### Running a subset of tests with marks

Tests can be tagged with arbitrary `marks` (inspired by `pytest -m`) and then
filtered at validation time. This is useful for:

* **Selective alerting** - run only `critical` health checks in production
* **Incident response** - quickly run `network` or `storage` categories
* **Flaky-test quarantine** - tag flaky tests `flaky` and exclude them from CI

Add marks to any resource in your gossfile:

```yaml
command:
echo hello:
exit-status: 0
stdout: [hello]
marks:
- critical
- fast
echo slow:
exit-status: 0
stdout: [slow]
marks:
- slow
http:
https://api.example.com/health:
status: 200
marks:
- critical
- network
```

Then filter at run time with `--marks` (include) and/or `--exclude-marks`:

```console
# Only run tests marked "critical"
$ goss validate --marks critical

# Run tests marked "critical" OR "fast"
$ goss validate --marks critical,fast

# Exclude slow and flaky tests
$ goss validate --exclude-marks slow,flaky

# Combine: run critical tests but skip any that are also marked flaky
$ goss validate --marks critical --exclude-marks flaky
```

Resources with **no marks** are always included unless filtered out by
`--exclude-marks` or other criteria. When `--marks` is set, unmarked resources
are skipped. See `docs/marks.md` or the design spec for the full evaluation
rules.

Marks also work on the health endpoint - pass them as query parameters, which
override any defaults baked in via the serve command's flags:

```console
$ goss serve --marks critical &
$ curl 'localhost:8080/healthz' # uses --marks critical
$ curl 'localhost:8080/healthz?marks=network' # overrides: only network
$ curl 'localhost:8080/healthz?exclude-marks=slow,flaky' # include all but slow/flaky
```

When using `goss add` or `goss autoadd`, pass `--marks` to tag newly created
resources automatically:

```console
$ goss add --marks critical service sshd
$ goss autoadd --marks network nginx
```

Existing marks on previously parsed resources are preserved; the flag only
applies to newly created ones.

### Manually editing Goss files

Goss files can be manually edited to improve readability and expressiveness of tests.
Expand Down
57 changes: 50 additions & 7 deletions add.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import (

// AddResources is a simple wrapper to add multiple resources
func AddResources(fileName, resourceName string, keys []string, c *util.Config) error {
var err error
err = setLogLevel(c)
err := setLogLevel(c)
if err != nil {
return err
}
outStoreFormat, err = getStoreFormatFromFileName(fileName)
storeFormat, err := getStoreFormatFromFileName(fileName)
if err != nil {
return err
}
setStoreFormat(storeFormat)

var gossConfig GossConfig
if _, err := os.Stat(fileName); err == nil {
Expand All @@ -41,7 +41,14 @@ func AddResources(fileName, resourceName string, keys []string, c *util.Config)
}
}

return WriteJSON(fileName, gossConfig)
warning, err := WriteJSON(fileName, gossConfig)
if err != nil {
return err
}
if warning != "" {
c.Log().Printf("%s", warning)
}
return nil
}

// AddResource adds a single resource to fileName
Expand Down Expand Up @@ -89,18 +96,37 @@ func AddResource(fileName string, gossConfig GossConfig, resourceName, key strin
return err
}

applyMarksIfUnset(res, config.IncludeMarks)

resourcePrint(fileName, res, config.AnnounceToCLI)

return nil
}

// applyMarksIfUnset sets the supplied marks on res when res has no existing
// marks. It is called from the add/autoadd paths so that --marks tags newly
// created resources but never overwrites marks that existed in a previously
// parsed gossfile. A nil or empty marks slice is a no-op.
func applyMarksIfUnset(res resource.ResourceRead, marks []string) {
if res == nil || len(marks) == 0 {
return
}
if len(res.GetMarks()) > 0 {
return
}
// Copy to avoid aliasing the config's slice into every resource.
out := make([]string, len(marks))
copy(out, marks)
res.SetMarks(out)
}

// AutoAddResources is a simple wrapper to add multiple resources
func AutoAddResources(fileName string, keys []string, c *util.Config) error {
var err error
outStoreFormat, err = getStoreFormatFromFileName(fileName)
storeFormat, err := getStoreFormatFromFileName(fileName)
if err != nil {
return err
}
setStoreFormat(storeFormat)

var gossConfig GossConfig
if _, err = os.Stat(fileName); err == nil {
Expand All @@ -120,18 +146,28 @@ func AutoAddResources(fileName string, keys []string, c *util.Config) error {
}
}

return WriteJSON(fileName, gossConfig)
warning, err := WriteJSON(fileName, gossConfig)
if err != nil {
return err
}
if warning != "" {
c.Log().Printf("%s", warning)
}
return nil
}

// AutoAddResource adds a single resource to fileName with automatic detection of the type of resource
func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *util.Config, sys *system.System) error {
marks := c.IncludeMarks

// file
if strings.Contains(key, "/") {
res, _, ok, err := gossConfig.Files.AppendSysResourceIfExists(key, sys)
if err != nil {
return err
}
if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
}
}
Expand All @@ -141,6 +177,7 @@ func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *util
return err

} else if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
}

Expand All @@ -150,6 +187,7 @@ func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *util
return err

} else if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
}

Expand All @@ -158,13 +196,15 @@ func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *util
return err

} else if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
}

// process
if res, sysres, ok, err := gossConfig.Processes.AppendSysResourceIfExists(key, sys); err != nil {
return err
} else if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
ports := system.GetPorts(true)
pids, _ := sysres.Pids()
Expand All @@ -177,6 +217,7 @@ func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *util
if res, _, ok, err := gossConfig.Ports.AppendSysResourceIfExists(port, sys); err != nil {
return err
} else if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
}
}
Expand All @@ -189,13 +230,15 @@ func AutoAddResource(fileName string, gossConfig GossConfig, key string, c *util
if res, _, ok, err := gossConfig.Services.AppendSysResourceIfExists(key, sys); err != nil {
return err
} else if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
}

// user
if res, _, ok, err := gossConfig.Users.AppendSysResourceIfExists(key, sys); err != nil {
return err
} else if ok {
applyMarksIfUnset(res, marks)
resourcePrint(fileName, res, c.AnnounceToCLI)
}

Expand Down
82 changes: 82 additions & 0 deletions add_marks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package goss

import (
"testing"

"github.com/goss-org/goss/resource"
"github.com/stretchr/testify/assert"
)

func TestApplyMarksIfUnset(t *testing.T) {
t.Parallel()

t.Run("nil resource is a no-op", func(t *testing.T) {
t.Parallel()
// Must not panic.
applyMarksIfUnset(nil, []string{"critical"})
})

t.Run("empty marks leaves resource untouched", func(t *testing.T) {
t.Parallel()
r := &resource.Addr{}
applyMarksIfUnset(r, nil)
assert.Empty(t, r.GetMarks())

applyMarksIfUnset(r, []string{})
assert.Empty(t, r.GetMarks())
})

t.Run("applies marks when resource has none", func(t *testing.T) {
t.Parallel()
r := &resource.Addr{}
applyMarksIfUnset(r, []string{"critical", "fast"})
assert.Equal(t, []string{"critical", "fast"}, r.GetMarks())
})

t.Run("preserves existing marks", func(t *testing.T) {
t.Parallel()
r := &resource.Addr{Marks: []string{"existing"}}
applyMarksIfUnset(r, []string{"critical"})
assert.Equal(t, []string{"existing"}, r.GetMarks())
})

t.Run("copies input slice to avoid aliasing", func(t *testing.T) {
t.Parallel()
r1 := &resource.Addr{}
r2 := &resource.Addr{}
input := []string{"a", "b"}

applyMarksIfUnset(r1, input)
applyMarksIfUnset(r2, input)

// Mutate r1's marks; r2 must not see the change.
r1.GetMarks()[0] = "mutated"
assert.Equal(t, []string{"a", "b"}, r2.GetMarks(),
"applyMarksIfUnset must copy the slice so resources don't share backing arrays")
})

t.Run("works across multiple resource types", func(t *testing.T) {
t.Parallel()
cases := []resource.ResourceRead{
&resource.Addr{},
&resource.Command{},
&resource.File{},
&resource.Group{},
&resource.HTTP{},
&resource.Package{},
&resource.Port{},
&resource.Process{},
&resource.Service{},
&resource.User{},
&resource.DNS{},
&resource.KernelParam{},
&resource.Mount{},
&resource.Interface{},
&resource.Gossfile{},
}
for _, r := range cases {
applyMarksIfUnset(r, []string{"production"})
assert.Equal(t, []string{"production"}, r.GetMarks())
}
})
}
Loading
Loading