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
2 changes: 1 addition & 1 deletion internal/commands/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func NewTestCommand(ctx context.Context) *cobra.Command {

cmd.Flags().StringSliceP("policy", "p", []string{"policy"}, "Path to the Rego policy files directory")
cmd.Flags().StringSliceP("update", "u", []string{}, "A list of URLs can be provided to the update flag, which will download before the tests run")
cmd.Flags().StringSliceP("namespace", "n", []string{"main"}, "Test policies in a specific namespace")
cmd.Flags().StringSliceP("namespace", "n", []string{"main"}, "Test policies in a specific namespace; supports glob patterns matched against loaded namespaces (e.g. \"k8s.simple.*\")")
cmd.Flags().StringSliceP("data", "d", []string{}, "A list of paths from which data for the rego policies will be recursively loaded")

cmd.Flags().StringSlice("proto-file-dirs", []string{}, "A list of directories containing Protocol Buffer definitions")
Expand Down
60 changes: 60 additions & 0 deletions runner/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/open-policy-agent/conftest/downloader"
"github.com/open-policy-agent/conftest/output"
Expand Down Expand Up @@ -89,6 +91,11 @@ func (t *TestRunner) Run(ctx context.Context, fileList []string) (output.CheckRe
namespaces := t.Namespace
if t.AllNamespaces {
namespaces = engine.Namespaces()
} else if hasGlob(namespaces) {
namespaces, err = expandNamespaceGlobs(namespaces, engine.Namespaces())
if err != nil {
return nil, fmt.Errorf("expand namespace globs: %w", err)
}
}

var results output.CheckResults
Expand All @@ -113,6 +120,59 @@ func (t *TestRunner) Run(ctx context.Context, fileList []string) (output.CheckRe
return results, nil
}

// hasGlob reports whether any namespace pattern contains a glob meta-character.
func hasGlob(patterns []string) bool {
for _, p := range patterns {
if strings.ContainsAny(p, "*?[") {
return true
}
}
return false
}

// expandNamespaceGlobs expands any glob patterns in patterns against the set
// of namespaces actually loaded into the engine. Non-glob patterns are passed
// through unchanged so users can mix literals and globs (e.g.
// --namespace main --namespace k8s.simple.*). Patterns are matched with
// path.Match semantics after substituting "." for "/" so that "*" matches a
// single dotted segment.
func expandNamespaceGlobs(patterns, available []string) ([]string, error) {
seen := make(map[string]struct{})
var result []string
add := func(ns string) {
if _, ok := seen[ns]; ok {
return
}
seen[ns] = struct{}{}
result = append(result, ns)
}

for _, pattern := range patterns {
if !strings.ContainsAny(pattern, "*?[") {
add(pattern)
continue
}

slashed := strings.ReplaceAll(pattern, ".", "/")
matched := false
for _, ns := range available {
ok, err := path.Match(slashed, strings.ReplaceAll(ns, ".", "/"))
if err != nil {
return nil, fmt.Errorf("invalid namespace pattern %q: %w", pattern, err)
}
if ok {
add(ns)
matched = true
}
}
if !matched {
return nil, fmt.Errorf("no namespaces matched pattern %q", pattern)
}
}

return result, nil
}

func parseFileList(fileList []string, ignoreRegex string) ([]string, error) {
var files []string
for _, file := range fileList {
Expand Down
92 changes: 92 additions & 0 deletions runner/test_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package runner

import (
"reflect"
"testing"
)

func TestHasGlob(t *testing.T) {
tests := []struct {
name string
patterns []string
want bool
}{
{name: "empty", patterns: nil, want: false},
{name: "literals only", patterns: []string{"main", "k8s.simple"}, want: false},
{name: "single star", patterns: []string{"k8s.*"}, want: true},
{name: "question mark", patterns: []string{"k8s.simpl?"}, want: true},
{name: "char class", patterns: []string{"k8s.[a-z]"}, want: true},
{name: "mixed", patterns: []string{"main", "k8s.*"}, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasGlob(tt.patterns); got != tt.want {
t.Errorf("hasGlob(%v) = %v, want %v", tt.patterns, got, tt.want)
}
})
}
}

func TestExpandNamespaceGlobs(t *testing.T) {
available := []string{
"main",
"k8s.simple.deployment",
"k8s.simple.hpa",
"k8s.simple.pod",
"k8s.combined.deployment",
"data.simple",
}

tests := []struct {
name string
patterns []string
want []string
wantErr bool
}{
{
name: "literal passes through",
patterns: []string{"main"},
want: []string{"main"},
},
{
name: "single segment wildcard",
patterns: []string{"k8s.simple.*"},
want: []string{"k8s.simple.deployment", "k8s.simple.hpa", "k8s.simple.pod"},
},
{
name: "wildcard does not cross dot boundaries",
patterns: []string{"k8s.*"},
want: nil,
wantErr: true,
},
{
name: "char class",
patterns: []string{"k8s.simple.[hp]*"},
want: []string{"k8s.simple.hpa", "k8s.simple.pod"},
},
{
name: "mix of literal and glob, dedupes",
patterns: []string{"main", "k8s.simple.*", "k8s.simple.pod"},
want: []string{"main", "k8s.simple.deployment", "k8s.simple.hpa", "k8s.simple.pod"},
},
{
name: "no match errors",
patterns: []string{"missing.*"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := expandNamespaceGlobs(tt.patterns, available)
if (err != nil) != tt.wantErr {
t.Fatalf("err = %v, wantErr = %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}