Skip to content

Commit

Permalink
cmd/exec: adds --std-in flag for input piping or manual entry
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Lacy <[email protected]>
  • Loading branch information
colinjlacy committed Jun 17, 2024
1 parent b463d30 commit 5fda392
Show file tree
Hide file tree
Showing 13 changed files with 975 additions and 156 deletions.
15 changes: 14 additions & 1 deletion cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -52,7 +53,7 @@ the OPA configuration) against each input file. This can be overridden by
specifying the --decision argument and pointing at a specific policy decision,
e.g., opa exec --decision /foo/bar/baz ...`,

Args: cobra.MinimumNArgs(1),
//Args: minimumArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
return env.CmdFlags.CheckEnvironmentVariables(cmd)
},
Expand All @@ -78,6 +79,7 @@ e.g., opa exec --decision /foo/bar/baz ...`,
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)")
cmd.Flags().BoolVarP(&params.StdIn, "std-in", "", false, "read input from os.Stdin rather than a static file; read timeout is 30 seconds")
cmd.Flags().DurationVar(&params.Timeout, "timeout", 0, "set exec timeout with a Go-style duration, such as '5m 30s'. (default unlimited)")
addV1CompatibleFlag(cmd.Flags(), &params.V1Compatible, false)

Expand All @@ -95,6 +97,10 @@ func runExec(params *exec.Params) error {
}

func runExecWithContext(ctx context.Context, params *exec.Params) error {
if minimumInputErr := validateMinimumInput(params); minimumInputErr != nil {
return minimumInputErr
}

stdLogger, consoleLogger, err := setupLogging(params.LogLevel.String(), params.LogFormat.String(), params.LogTimestampFormat)
if err != nil {
return fmt.Errorf("config error: %w", err)
Expand Down Expand Up @@ -258,3 +264,10 @@ func injectExplicitBundles(root map[string]interface{}, paths []string) error {

return nil
}

func validateMinimumInput(params *exec.Params) error {
if !params.StdIn && len(params.Paths) == 0 {
return errors.New("requires at least 1 path arg, or the --std-in flag")
}
return nil
}
129 changes: 129 additions & 0 deletions cmd/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,135 @@ func TestFailFlagCases(t *testing.T) {
}
}

func TestExecWithInvalidInputOptions(t *testing.T) {
tests := []struct {
description string
files map[string]string
stdIn bool
input string
expectError bool
expected string
}{
{
description: "path passed in as arg should not raise error",
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: "",
},
{
description: "no paths passed in as args should raise error if --std-in flag not set",
files: map[string]string{
"bundle/x.rego": `package system
test_fun := x {
x = false
x
}
undefined_test {
test_fun
}`,
},
expectError: true,
expected: "requires at least 1 path arg, or the --std-in flag",
},
{
description: "should not raise error if --std-in flag is set when no paths passed in as args",
files: map[string]string{
"bundle/x.rego": `package system
test_fun := x {
x = false
x
}
undefined_test {
test_fun
}`,
},
stdIn: true,
input: `{"foo": 7}`,
expectError: false,
expected: "",
},
{
description: "should raise error if --std-in flag is set and no input is given",
files: map[string]string{
"bundle/x.rego": `package system
test_fun := x {
x = false
x
}
undefined_test {
test_fun
}`,
},
stdIn: true,
input: "",
expectError: true,
expected: "exec error: cannot execute on empty input; please enter valid json or yaml when using the --std-in flag",
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
test.WithTempFS(tt.files, func(dir string) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.BundlePaths = []string{dir + "/bundle/"}
if tt.stdIn {
params.StdIn = true
tempFile, err := os.CreateTemp("", "test")
if err != nil {
t.Fatalf("unexpected error creating temp file: %q", err.Error())
}
if _, err := tempFile.Write([]byte(tt.input)); err != nil {
t.Fatalf("unexpeced error when writing to temp file: %q", err.Error())
}
if _, err := tempFile.Seek(0, 0); err != nil {
t.Fatalf("unexpected error when rewinding temp file: %q", err.Error())
}
oldStdin := os.Stdin
defer func() {
os.Stdin = oldStdin
os.Remove(tempFile.Name())
}()
os.Stdin = tempFile
} else {
if _, ok := tt.files["files/test.json"]; ok {
params.Paths = append(params.Paths, dir+"/files/")
}
}

err := runExec(params)
if err != nil && !tt.expectError {
t.Fatalf("unexpected error in test: %q", err.Error())
}
if err == nil && tt.expectError {
t.Fatalf("expected error %q, but none occurred in test", tt.expected)
}
if err != nil && err.Error() != tt.expected {
t.Fatalf("expected error %q, but got %q", tt.expected, err.Error())
}
})
})
}
}

func TestExecTimeoutWithMalformedRemoteBundle(t *testing.T) {
test.WithTempFS(map[string]string{}, func(dir string) {
// Note(philipc): We add the "raw bundles" flag so that we can stuff a
Expand Down
Loading

0 comments on commit 5fda392

Please sign in to comment.