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
1 change: 1 addition & 0 deletions internal/commands/examples.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func showExamples() string {
newExample("show dev -C postgres -C redis", "expand all but 2 components"),
newExample("show dev -k deployment -k configmap", "show only deployments and config maps"),
newExample("show dev -K secret", "show all objects except secrets"),
newExample("show dev -t 'Deployment/(redis.*)'", "show only deployments whose name starts with 'redis'"),
newExample("show dev -O", "list all objects for the dev environment"),
)
}
Expand Down
29 changes: 29 additions & 0 deletions internal/model/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package model

import (
"fmt"
"regexp"
"slices"
"strings"

"k8s.io/gengo/namer"
Expand Down Expand Up @@ -110,3 +112,30 @@ func newKindFilter(includes, excludes []string) (Filter, error) {
}
return bf, nil
}

type regexpFilter struct {
includes []*regexp.Regexp
excludes []*regexp.Regexp
}

func (r *regexpFilter) HasFilters() bool {
return len(r.includes) > 0 || len(r.excludes) > 0
}

func (r *regexpFilter) ShouldInclude(s string) bool {
if len(r.includes) > 0 && !slices.ContainsFunc(r.includes, func(re *regexp.Regexp) bool {
return re.MatchString(s)
}) {
return false
}
return !slices.ContainsFunc(r.excludes, func(re *regexp.Regexp) bool {
return re.MatchString(s)
})
}

// newRegexpFilter returns a filter that matches values against the supplied compiled regular
// expressions. A value is included if it matches at least one include pattern and none of the
// exclude patterns.
func newRegexpFilter(includes, excludes []*regexp.Regexp) Filter {
return &regexpFilter{includes: includes, excludes: excludes}
}
61 changes: 61 additions & 0 deletions internal/model/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,37 @@ package model
import (
"testing"

"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func newTestObject(kind, name string) K8sQbecMeta {
return NewK8sLocalObject(map[string]interface{}{
"apiVersion": "v1",
"kind": kind,
"metadata": map[string]interface{}{"name": name},
}, LocalAttrs{Component: "comp"})
}

// newTestFiltersErr parses the supplied flags through the filters setup and returns the filters
// along with any setup error.
func newTestFiltersErr(t *testing.T, args ...string) (Filters, error) {
t.Helper()
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
fn := NewFilters(flags, true)
require.Nil(t, flags.Parse(args))
return fn()
}

// newTestFilters parses the supplied flags through the filters setup and returns the filters.
func newTestFilters(t *testing.T, args ...string) Filters {
t.Helper()
f, err := newTestFiltersErr(t, args...)
require.Nil(t, err)
return f
}

func TestComponentFilterIncludes(t *testing.T) {
filter, err := NewComponentFilter([]string{"foo", "bar"}, []string{})
require.Nil(t, err)
Expand Down Expand Up @@ -118,3 +145,37 @@ func TestKindFilterBad(t *testing.T) {
require.NotNil(t, err)
require.Equal(t, "cannot include as well as exclude kinds, specify one or the other", err.Error())
}

func TestObjectPatternFilterIncludes(t *testing.T) {
f := newTestFilters(t,
"--object-pattern=Deployment/(foo.*)",
"--object-pattern=ConfigMap/(bar.*)",
)
a := assert.New(t)
a.True(f.ObjectFilter(newTestObject("Deployment", "foo-1")))
a.True(f.ObjectFilter(newTestObject("Deployment", "FOO-1"))) // matching is case-insensitive
a.True(f.ObjectFilter(newTestObject("ConfigMap", "bar-2")))
a.False(f.ObjectFilter(newTestObject("Service", "foo-1")))
a.False(f.ObjectFilter(newTestObject("Deployment", "baz-1")))
}

func TestObjectPatternFilterExcludes(t *testing.T) {
f := newTestFilters(t, "--exclude-object-pattern=Deployment/(foo.*)")
a := assert.New(t)
a.True(f.ObjectFilter(newTestObject("ConfigMap", "foo-1")))
a.True(f.ObjectFilter(newTestObject("Deployment", "bar-1")))
a.False(f.ObjectFilter(newTestObject("Deployment", "foo-1")))
}

func TestObjectPatternFilterNoFilters(t *testing.T) {
f := newTestFilters(t)
a := assert.New(t)
a.True(f.ObjectFilter(newTestObject("Deployment", "foo-1")))
a.True(f.ObjectFilter(newTestObject("Service", "bar-1")))
}

func TestObjectPatternFilterInvalid(t *testing.T) {
_, err := newTestFiltersErr(t, "--object-pattern=([")
require.NotNil(t, err)
require.Contains(t, err.Error(), `invalid --object-pattern regexp`)
}
43 changes: 42 additions & 1 deletion internal/model/filters.go

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should name this flag --object-pattern and --exclude-object-pattern. There is an existing flag --objects and that is too close to --object.

Can you also update site/content/userguide/usage/commands.md and internal/commands/show.go?

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package model

import (
"fmt"
"regexp"

"github.com/pkg/errors"
"github.com/spf13/pflag"
Expand All @@ -31,6 +32,7 @@ type Namespaced interface {
type Filters struct {
includes []string
excludes []string
objectPatternFilter Filter
excludeClusterObjects bool
kindFilter Filter
componentFilter Filter
Expand All @@ -39,12 +41,14 @@ type Filters struct {

// NewFilters sets up options in the supplied flags and returns a function to return filters.
func NewFilters(flags *pflag.FlagSet, includeAllFilters bool) func() (Filters, error) {
var includes, excludes, kindIncludes, kindExcludes, nsIncludes, nsExcludes []string
var includes, excludes, kindIncludes, kindExcludes, nsIncludes, nsExcludes, regexpObjectIncludes, regexpObjectExcludes []string
var includeClusterScopedObjects bool

flags.StringArrayVarP(&includes, "component", "c", nil, "include just this component")
flags.StringArrayVarP(&excludes, "exclude-component", "C", nil, "exclude this component")
if includeAllFilters {
flags.StringArrayVarP(&regexpObjectIncludes, "object-pattern", "t", nil, "include k8s objects whose Kind/Name matches this regexp")
flags.StringArrayVarP(&regexpObjectExcludes, "exclude-object-pattern", "T", nil, "exclude k8s objects whose Kind/Name matches this regexp")
flags.StringArrayVarP(&kindIncludes, "kind", "k", nil, "include objects with this kind")
flags.StringArrayVarP(&kindExcludes, "exclude-kind", "K", nil, "exclude objects with this kind")
flags.StringArrayVarP(&nsIncludes, "include-namespace", "p", nil, "include objects with this namespace")
Expand All @@ -69,9 +73,31 @@ func NewFilters(flags *pflag.FlagSet, includeAllFilters bool) func() (Filters, e
includeClusterScopedObjects = false
}
}

compilePatterns := func(patterns []string, flag string) ([]*regexp.Regexp, error) {
ret := make([]*regexp.Regexp, 0, len(patterns))
for _, pattern := range patterns {
compiled, err := regexp.Compile(fmt.Sprintf(`(?i)^(?:%s)$`, pattern))
if err != nil {
return nil, errors.Wrapf(err, "invalid --%s regexp %q", flag, pattern)
}
ret = append(ret, compiled)
}
return ret, nil
}
regexpIncludes, err := compilePatterns(regexpObjectIncludes, "object-pattern")
if err != nil {
return Filters{}, err
}
regexpExcludes, err := compilePatterns(regexpObjectExcludes, "exclude-object-pattern")
if err != nil {
return Filters{}, err
}

return Filters{
includes: includes,
excludes: excludes,
objectPatternFilter: newRegexpFilter(regexpIncludes, regexpExcludes),
kindFilter: of,
componentFilter: cf,
namespaceFilter: nf,
Expand All @@ -95,6 +121,18 @@ func (f Filters) GVKFilter(gvk schema.GroupVersionKind) bool {
return f.kindFilter != nil && f.kindFilter.ShouldInclude(gvk.Kind)
}

// ObjectFilter returns true if the object matches the object pattern filters. Matching is
// case-insensitive against the Kind/Name of the object, with each pattern anchored to both ends.
// When several include patterns are supplied, they are combined with a logical OR (union semantics).
// This code was inspired and adapted from grafana tanka
// https://github.com/grafana/tanka/blob/a6a63ac17f713d5fd64bb6d7972bc84c6b5902a6/pkg/process/filter.go#L74
func (f Filters) ObjectFilter(object K8sQbecMeta) bool {
if f.objectPatternFilter == nil || !f.objectPatternFilter.HasFilters() {
return true
}
return f.objectPatternFilter.ShouldInclude(object.GetKind() + "/" + object.GetName())
}

// HasNamespaceFilters returns true if filters based on namespace scope are in effect.
func (f Filters) HasNamespaceFilters() bool {
return (f.namespaceFilter != nil && f.namespaceFilter.HasFilters()) || f.excludeClusterObjects
Expand All @@ -103,6 +141,9 @@ func (f Filters) HasNamespaceFilters() bool {
// Match returns true if the current filters match the supplied object. The client can be nil
// if namespace scope filters are not in effect.
func (f Filters) Match(o K8sQbecMeta, client Namespaced, defaultNS string) (bool, error) {
if !f.ObjectFilter(o) {
return false, nil
}
if f.HasNamespaceFilters() && client == nil {
return false, fmt.Errorf("no namespace metadata when namespace filters present")
}
Expand Down
42 changes: 33 additions & 9 deletions site/content/userguide/usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ cluster scoped objects.
*Note:* specifying namespace / cluster-scope filters requires qbec to access the cluster in order to retrieve metadata
on object kinds. This means that a `qbec show` command that normally does not need cluster access will now require it.

### Object pattern filters

Object pattern filters allow you to filter objects by matching their `Kind/Name` string against a regular expression.

To include objects matching a pattern, use `-t pattern1 -t pattern2 ...`

To exclude objects matching a pattern, use `-T pattern1 -T pattern2 ...`

Patterns are matched case-insensitively against the `Kind/Name` string of every object and are anchored to both ends.
For instance, `-t 'Deployment/(redis.*)'` matches all objects of kind `Deployment` whose name starts with `redis`.

When multiple include patterns are specified, an object is included if it matches any of them. Similarly, when multiple
exclude patterns are specified, an object is excluded if it matches any of them. Unlike kind and component filters,
include and exclude patterns can be combined; objects matching an exclude pattern are always excluded.

## Command help

Help and examples for every sub-command can be displayed with a `--help` flag.
Expand Down Expand Up @@ -139,19 +154,28 @@ qbec show dev -k deployment -k configmap
# show all objects except secrets
qbec show dev -K secret

# show only deployments whose name starts with 'redis'
qbec show dev -t 'Deployment/(redis.*)'

# list all objects for the dev environment
qbec show dev -O

Flags:
-c, --component stringArray include just this component
-C, --exclude-component stringArray exclude this component
-K, --exclude-kind stringArray exclude objects with this kind
-o, --format string Output format. Supported values are: json, yaml (default "yaml")
-h, --help help for show
-k, --kind stringArray include objects with this kind
-O, --objects Only print names of objects instead of their contents
-S, --show-secrets do not obfuscate secret values in the output
--sort-apply sort output in apply order (requires cluster access)
--clean do not display qbec-generated labels and annotations
-c, --component stringArray include just this component
-C, --exclude-component stringArray exclude this component
-K, --exclude-kind stringArray exclude objects with this kind
-P, --exclude-namespace stringArray exclude objects with this namespace
-T, --exclude-object-pattern stringArray exclude k8s objects whose Kind/Name matches this regexp
-o, --format string Output format. Supported values are: json, yaml (default "yaml")
-h, --help help for show
--include-cluster-objects include cluster scoped objects, false by default when namespace filters present (default true)
-p, --include-namespace stringArray include objects with this namespace
-k, --kind stringArray include objects with this kind
-t, --object-pattern stringArray include k8s objects whose Kind/Name matches this regexp
-O, --objects Only print names of objects instead of their contents
-S, --show-secrets do not obfuscate secret values in the output
--sort-apply sort output in apply order (requires cluster access)

Use "qbec options" for a list of global options available to all commands.
```
Expand Down
Loading