Skip to content
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,77 @@ $ 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`:

```sh
# 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:

```sh
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:

```sh
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