Skip to content

Commit

Permalink
feat(cmd): Adding fail-non-empty flag (#6153)
Browse files Browse the repository at this point in the history
Add fail-non-empty flag to opa exec

Signed-off-by: Ronnie Personal <[email protected]>
  • Loading branch information
Ronnie-personal authored Aug 21, 2023
1 parent d52ba53 commit c3854aa
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 22 deletions.
5 changes: 3 additions & 2 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ e.g., opa exec --decision /foo/bar/baz ...`,
addConfigOverrides(cmd.Flags(), &params.ConfigOverrides)
addConfigOverrideFiles(cmd.Flags(), &params.ConfigOverrideFiles)
cmd.Flags().StringVarP(&params.Decision, "decision", "", "", "set decision to evaluate")
cmd.Flags().BoolVarP(&params.FailDefined, "fail-defined", "", false, "exits with non-zero exit code on defined/non-empty result and errors")
cmd.Flags().BoolVarP(&params.Fail, "fail", "", false, "exits with non-zero exit code on undefined/empty result and errors")
cmd.Flags().BoolVarP(&params.FailDefined, "fail-defined", "", false, "exits with non-zero exit code on defined result and errors")
cmd.Flags().BoolVarP(&params.Fail, "fail", "", false, "exits with non-zero exit code on undefined result and errors")
cmd.Flags().BoolVarP(&params.FailNonEmpty, "fail-non-empty", "", false, "exits with non-zero exit code on non-empty result and errors")
cmd.Flags().VarP(params.LogLevel, "log-level", "l", "set log level")
cmd.Flags().Var(params.LogFormat, "log-format", "set log format")
cmd.Flags().StringVar(&params.LogTimestampFormat, "log-timestamp-format", "", "set log timestamp format (OPA_LOG_TIMESTAMP_FORMAT environment variable)")
Expand Down
186 changes: 175 additions & 11 deletions cmd/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,54 @@ func TestInvalidConfig(t *testing.T) {
}
}

func TestInvalidConfigAllThree(t *testing.T) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.Fail = true
params.FailDefined = true
params.FailNonEmpty = true

err := exec.Exec(context.TODO(), nil, params)
if err == nil || err.Error() != "specify --fail or --fail-defined but not both" {
t.Fatalf("Expected error '%s' but got '%s'", "specify --fail or --fail-defined but not both", err.Error())
}
}

func TestInvalidConfigNonEmptyAndFail(t *testing.T) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.FailNonEmpty = true
params.Fail = true

err := exec.Exec(context.TODO(), nil, params)
if err == nil || err.Error() != "specify --fail-non-empty or --fail but not both" {
t.Fatalf("Expected error '%s' but got '%s'", "specify --fail-non-empty or --fail but not both", err.Error())
}
}

func TestInvalidConfigNonEmptyAndFailDefined(t *testing.T) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.FailNonEmpty = true
params.FailDefined = true

err := exec.Exec(context.TODO(), nil, params)
if err == nil || err.Error() != "specify --fail-non-empty or --fail-defined but not both" {
t.Fatalf("Expected error '%s' but got '%s'", "specify --fail-non-empty or --fail-defined but not both", err.Error())
}
}

func TestFailFlagCases(t *testing.T) {

var tests = []struct {
description string
files map[string]string
decision string
expectError bool
expected interface{}
fail bool
failDefined bool
description string
files map[string]string
decision string
expectError bool
expected interface{}
fail bool
failDefined bool
failNonEmpty bool
}{
{
description: "--fail-defined with undefined result",
Expand All @@ -187,6 +225,7 @@ func TestFailFlagCases(t *testing.T) {
test_fun
}`,
},
expectError: false,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"error": {
Expand Down Expand Up @@ -287,14 +326,15 @@ func TestFailFlagCases(t *testing.T) {
main["hello"]`,
},
expectError: false,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": ["hello"]
}]}`)),
fail: true,
},
{
description: "--fail-defined with true boolean result",
description: "--fail with true boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.defined.flag
Expand All @@ -308,15 +348,16 @@ func TestFailFlagCases(t *testing.T) {
some_function
}`,
},
decision: "fail/defined/flag/fail_test",
decision: "fail/defined/flag/fail_test",
expectError: false,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": true
}]}`)),
fail: true,
},
{
description: "--fail-defined with false boolean result",
description: "--fail with false boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.defined.flag
Expand All @@ -326,13 +367,135 @@ func TestFailFlagCases(t *testing.T) {
false
}`,
},
decision: "fail/defined/flag/fail_test",
decision: "fail/defined/flag/fail_test",
expectError: false,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": false
}]}`)),
fail: true,
},
{
description: "--fail-non-empty with undefined result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
test_fun := x {
x = false
x
}
undefined_test {
test_fun
}`,
},
expectError: false,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"error": {
"code": "opa_undefined_error",
"message": "/system/main decision was undefined"
}
}]}`)),
failNonEmpty: true,
},
{
description: "--fail-non-empty with populated result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
main["hello"]`,
},
decision: "",
expectError: true,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": ["hello"]
}]}`)),
failNonEmpty: true,
},
{
description: "--fail-non-empty with true boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
some_function {
input.foo == 7
}
default fail_test := false
fail_test {
some_function
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: true,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": true
}]}`)),
failNonEmpty: true,
},
{
description: "--fail-non-empty with false boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
default fail_test := false
fail_test {
false
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: true,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": false
}]}`)),
failNonEmpty: true,
},
{
description: "--fail-non-empty with an empty array",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
default fail_test := ["something", "hello"]
fail_test := [] if {
input.foo == 7
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: false,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": []
}]}`)),
failNonEmpty: true,
},
{
description: "--fail-non-empty for an empty set coming from a partial rule",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
fail_test[message] {
false
message := "not gonna happen"
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: false,
expected: util.MustUnmarshalJSON([]byte(`{"result": [{
"path": "/files/test.json",
"result": []
}]}`)),
failNonEmpty: true,
},
}

for _, tt := range tests {
Expand All @@ -348,6 +511,7 @@ func TestFailFlagCases(t *testing.T) {
}
params.FailDefined = tt.failDefined
params.Fail = tt.fail
params.FailNonEmpty = tt.failNonEmpty

err := runExec(params)
if err != nil && !tt.expectError {
Expand Down
31 changes: 24 additions & 7 deletions cmd/internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ type Params struct {
LogTimestampFormat string // log timestamp format for plugins
BundlePaths []string // explicit paths of bundles to inject into the configuration
Decision string // decision to evaluate (overrides default decision set by configuration)
Fail bool // exits with non-zero exit code on undefined/empty result and errors
FailDefined bool // exits with non-zero exit code on defined/non-empty result and errors
Fail bool // exits with non-zero exit code on undefined policy decision or empty policy decision result or other errors
FailDefined bool // exits with non-zero exit code on 'not undefined policy decisiondefined' or 'not empty policy decision result' or other errors
FailNonEmpty bool // exits with non-zero exit code on non-empty set (array) results
}

func NewParams(w io.Writer) *Params {
Expand All @@ -44,6 +45,12 @@ func (p *Params) validateParams() error {
if p.Fail && p.FailDefined {
return errors.New("specify --fail or --fail-defined but not both")
}
if p.FailNonEmpty && p.Fail {
return errors.New("specify --fail-non-empty or --fail but not both")
}
if p.FailNonEmpty && p.FailDefined {
return errors.New("specify --fail-non-empty or --fail-defined but not both")
}
return nil
}

Expand Down Expand Up @@ -79,7 +86,7 @@ func Exec(ctx context.Context, opa *sdk.OPA, params *Params) error {
if err2 := r.Report(result{Path: item.Path, Error: err}); err2 != nil {
return err2
}
if params.FailDefined || params.Fail {
if params.FailDefined || params.Fail || params.FailNonEmpty {
errorCount++
}
continue
Expand All @@ -96,7 +103,7 @@ func Exec(ctx context.Context, opa *sdk.OPA, params *Params) error {
if err2 := r.Report(result{Path: item.Path, Error: err}); err2 != nil {
return err2
}
if (params.FailDefined && !sdk.IsUndefinedErr(err)) || (params.Fail && sdk.IsUndefinedErr(err)) {
if (params.FailDefined && !sdk.IsUndefinedErr(err)) || (params.Fail && sdk.IsUndefinedErr(err)) || (params.FailNonEmpty && !sdk.IsUndefinedErr(err)) {
errorCount++
}
continue
Expand All @@ -109,17 +116,27 @@ func Exec(ctx context.Context, opa *sdk.OPA, params *Params) error {
if (params.FailDefined && rs.Result != nil) || (params.Fail && rs.Result == nil) {
failCount++
}
}

if params.FailNonEmpty && rs.Result != nil {
// Check if rs.Result is an array and has one or more members
resultArray, isArray := rs.Result.([]interface{})
if (!isArray) || (isArray && (len(resultArray) > 0)) {
failCount++
}
}
}
if err := r.Close(); err != nil {
return err
}

if (params.Fail || params.FailDefined) && (failCount > 0 || errorCount > 0) {
if (params.Fail || params.FailDefined || params.FailNonEmpty) && (failCount > 0 || errorCount > 0) {
if params.Fail {
return fmt.Errorf("there were %d failures and %d errors counted in the results list, and --fail is set", failCount, errorCount)
}
return fmt.Errorf("there were %d failures and %d errors counted in the results list, and --fail-defined is set", failCount, errorCount)
if params.FailDefined {
return fmt.Errorf("there were %d failures and %d errors counted in the results list, and --fail-defined is set", failCount, errorCount)
}
return fmt.Errorf("there were %d failures and %d errors counted in the results list, and --fail-non-empty is set", failCount, errorCount)
}

return nil
Expand Down
5 changes: 3 additions & 2 deletions docs/content/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,9 @@ opa exec <path> [<path> [...]] [flags]
-b, --bundle string set bundle file(s) or directory path(s). This flag can be repeated.
-c, --config-file string set path of configuration file
--decision string set decision to evaluate
--fail exits with non-zero exit code on undefined/empty result and errors
--fail-defined exits with non-zero exit code on defined/non-empty result and errors
--fail
--fail-defined
--fail-non-empty
-f, --format {pretty,json} set output format (default pretty)
-h, --help help for exec
--log-format {text,json,json-pretty} set log format (default json)
Expand Down

0 comments on commit c3854aa

Please sign in to comment.