Skip to content

Commit ea0e8cd

Browse files
committed
feat: add reformat command for JSON output conversion
Add new reformat command that converts conftest JSON output to different formats (table, junit, sarif, etc). Supports both positional arguments and stdin input, aligning with existing conftest command patterns. - Use positional args for input files: reformat file.json - Support stdin input for piping: conftest test | reformat - Add comprehensive BATS tests Signed-off-by: Ville Vesilehto <[email protected]>
1 parent ebb167d commit ea0e8cd

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed

internal/commands/default.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func NewDefaultCommand() *cobra.Command {
6060
cmd.AddCommand(NewVerifyCommand(ctx))
6161
cmd.AddCommand(NewPluginCommand(ctx))
6262
cmd.AddCommand(NewFormatCommand())
63+
cmd.AddCommand(NewReformatCommand())
6364
cmd.AddCommand(NewDocumentCommand())
6465

6566
plugins, err := plugin.FindAll()

internal/commands/reformat.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package commands
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
9+
"github.com/open-policy-agent/conftest/output"
10+
"github.com/spf13/cobra"
11+
"github.com/spf13/viper"
12+
)
13+
14+
const reformatDesc = `
15+
This command reformats conftest JSON output to different formats.
16+
17+
The reformat command takes JSON output from conftest (typically from stdin or a file)
18+
and converts it to other supported output formats. This allows for decoupling test
19+
execution from formatting, enabling multiple output formats from a single test run.
20+
21+
Usage examples:
22+
23+
# Convert JSON output to table format
24+
$ conftest test --output json config.yaml | conftest reformat --output table
25+
26+
# Convert JSON file to JUnit format
27+
$ conftest reformat --output junit results.json
28+
29+
# Convert JSON to multiple formats
30+
$ conftest test --output json config.yaml > results.json
31+
$ conftest reformat --output table results.json
32+
$ conftest reformat --output junit results.json
33+
34+
Supported output formats: %s`
35+
36+
// NewReformatCommand creates a reformat command.
37+
// This command can be used for reformatting conftest JSON output to different formats.
38+
func NewReformatCommand() *cobra.Command {
39+
cmd := cobra.Command{
40+
Use: "reformat [file...]",
41+
Short: "Reformat conftest JSON output to different formats",
42+
Long: fmt.Sprintf(reformatDesc, output.Outputs()),
43+
PreRunE: func(cmd *cobra.Command, _ []string) error {
44+
flagNames := []string{
45+
"output",
46+
}
47+
for _, name := range flagNames {
48+
if err := viper.BindPFlag(name, cmd.Flags().Lookup(name)); err != nil {
49+
return fmt.Errorf("bind flag: %w", err)
50+
}
51+
}
52+
53+
return nil
54+
},
55+
RunE: func(_ *cobra.Command, args []string) error {
56+
outputFormat := viper.GetString("output")
57+
58+
// Determine input source: positional args or stdin
59+
var reader io.Reader
60+
if len(args) > 0 {
61+
// Use first positional argument as input file
62+
file, err := os.Open(args[0])
63+
if err != nil {
64+
return fmt.Errorf("failed to open input file: %w", err)
65+
}
66+
defer file.Close()
67+
reader = file
68+
} else {
69+
// No positional args, read from stdin
70+
reader = os.Stdin
71+
}
72+
73+
// Parse JSON input
74+
var results output.CheckResults
75+
decoder := json.NewDecoder(reader)
76+
if err := decoder.Decode(&results); err != nil {
77+
return fmt.Errorf("failed to parse JSON input: %w", err)
78+
}
79+
80+
// Create outputter
81+
outputter := output.Get(outputFormat, output.Options{})
82+
83+
// Format and output results
84+
if err := outputter.Output(results); err != nil {
85+
return fmt.Errorf("failed to output results: %w", err)
86+
}
87+
88+
return nil
89+
},
90+
}
91+
92+
cmd.Flags().StringP("output", "o", output.OutputStandard, fmt.Sprintf("Output format for conftest results - valid options are: %s", output.Outputs()))
93+
94+
return &cmd
95+
}

internal/commands/reformat_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package commands
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"os"
7+
"strings"
8+
"testing"
9+
10+
"github.com/open-policy-agent/conftest/output"
11+
"github.com/spf13/viper"
12+
)
13+
14+
func TestReformatCommand(t *testing.T) {
15+
sampleResults := output.CheckResults{
16+
{
17+
FileName: "test.yaml",
18+
Namespace: "main",
19+
Successes: 1,
20+
Warnings: []output.Result{
21+
{
22+
Message: "Warning: test warning",
23+
},
24+
},
25+
Failures: []output.Result{
26+
{
27+
Message: "Error: test failure",
28+
},
29+
},
30+
},
31+
}
32+
33+
testCases := []struct {
34+
name string
35+
outputFormat string
36+
expectError bool
37+
expectedOutput string
38+
}{
39+
{
40+
name: "json output",
41+
outputFormat: "json",
42+
expectError: false,
43+
},
44+
{
45+
name: "table output",
46+
outputFormat: "table",
47+
expectError: false,
48+
},
49+
{
50+
name: "tap output",
51+
outputFormat: "tap",
52+
expectError: false,
53+
},
54+
{
55+
name: "junit output",
56+
outputFormat: "junit",
57+
expectError: false,
58+
},
59+
{
60+
name: "invalid output format",
61+
outputFormat: "invalid",
62+
expectError: true,
63+
},
64+
}
65+
66+
for _, tc := range testCases {
67+
t.Run(tc.name, func(t *testing.T) {
68+
// Reset viper for each test
69+
viper.Reset()
70+
71+
jsonInput, err := json.Marshal(sampleResults)
72+
if err != nil {
73+
t.Fatalf("Failed to marshal test data: %v", err)
74+
}
75+
76+
var outputBuffer bytes.Buffer
77+
78+
// Temporarily redirect stdout
79+
oldStdout := os.Stdout
80+
defer func() {
81+
os.Stdout = oldStdout
82+
}()
83+
84+
cmd := NewReformatCommand()
85+
cmd.SetArgs([]string{})
86+
if err := cmd.Flags().Set("output", tc.outputFormat); err != nil {
87+
t.Error("failed to set flags for reformat command", err)
88+
}
89+
90+
// Create a pipe to simulate stdin
91+
reader := strings.NewReader(string(jsonInput))
92+
oldStdin := os.Stdin
93+
defer func() {
94+
os.Stdin = oldStdin
95+
}()
96+
97+
// Test command creation and flag parsing
98+
if !tc.expectError {
99+
100+
outputFlag := cmd.Flags().Lookup("output")
101+
if outputFlag == nil {
102+
t.Error("Expected output flag to exist")
103+
}
104+
105+
inputFlag := cmd.Flags().Lookup("input")
106+
if inputFlag == nil {
107+
t.Error("Expected input flag to exist")
108+
}
109+
110+
// Test that PreRunE doesn't error
111+
err := cmd.PreRunE(cmd, []string{})
112+
if err != nil {
113+
t.Errorf("PreRunE failed: %v", err)
114+
}
115+
}
116+
117+
// Restore stdin
118+
_ = reader
119+
_ = outputBuffer
120+
})
121+
}
122+
}
123+
124+
func TestReformatCommandFlags(t *testing.T) {
125+
cmd := NewReformatCommand()
126+
127+
// Test default values
128+
outputFlag := cmd.Flags().Lookup("output")
129+
if outputFlag == nil {
130+
t.Fatal("Expected output flag to exist")
131+
}
132+
if outputFlag.DefValue != output.OutputStandard {
133+
t.Errorf("Expected default output format to be %s, got %s", output.OutputStandard, outputFlag.DefValue)
134+
}
135+
136+
inputFlag := cmd.Flags().Lookup("input")
137+
if inputFlag == nil {
138+
t.Fatal("Expected input flag to exist")
139+
}
140+
if inputFlag.DefValue != "" {
141+
t.Errorf("Expected default input to be empty, got %s", inputFlag.DefValue)
142+
}
143+
144+
}
145+
146+
func TestReformatCommandPreRunE(t *testing.T) {
147+
cmd := NewReformatCommand()
148+
149+
// Test that PreRunE binds flags correctly
150+
err := cmd.PreRunE(cmd, []string{})
151+
if err != nil {
152+
t.Errorf("PreRunE failed: %v", err)
153+
}
154+
}

tests/reformat/test.bats

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env bats
2+
3+
DIR="$( cd "$( dirname "${BATS_TEST_FILENAME}" )" >/dev/null 2>&1 && pwd )"
4+
PROJECT_ROOT="$( cd "$DIR/../.." >/dev/null 2>&1 && pwd )"
5+
6+
setup_file() {
7+
cd "$PROJECT_ROOT"
8+
9+
# Generate test JSON data by running conftest test (ignore exit status since policy may fail)
10+
$CONFTEST test --output json -p examples/kubernetes/policy examples/kubernetes/deployment.yaml > "$DIR/test_results.json" || true
11+
}
12+
13+
teardown_file() {
14+
# Clean up generated test file
15+
rm -f "$DIR/test_results.json"
16+
rm -f "$DIR/empty.json"
17+
}
18+
19+
@test "Reformat JSON to table format using positional argument" {
20+
run $CONFTEST reformat "$DIR/test_results.json" --output table
21+
[ "$status" -eq 0 ]
22+
[[ "$output" =~ "RESULT" ]]
23+
[[ "$output" =~ "FILE" ]]
24+
[[ "$output" =~ "examples/kubernetes/deployment.yaml" ]]
25+
}
26+
27+
@test "Reformat JSON to junit format using positional argument" {
28+
run $CONFTEST reformat "$DIR/test_results.json" --output junit
29+
[ "$status" -eq 0 ]
30+
[[ "$output" =~ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" ]]
31+
[[ "$output" =~ "<testsuites>" ]]
32+
[[ "$output" =~ "hello-kubernetes" ]]
33+
}
34+
35+
36+
@test "Reformat JSON via stdin to table format" {
37+
run bash -c "cat \"$DIR/test_results.json\" | $CONFTEST reformat --output table"
38+
[ "$status" -eq 0 ]
39+
[[ "$output" =~ "RESULT" ]]
40+
[[ "$output" =~ "FILE" ]]
41+
[[ "$output" =~ "examples/kubernetes/deployment.yaml" ]]
42+
}
43+
44+
@test "Reformat JSON via stdin to json format (default)" {
45+
run bash -c "cat \"$DIR/test_results.json\" | $CONFTEST reformat"
46+
[ "$status" -eq 0 ]
47+
[[ "$output" =~ "examples/kubernetes/deployment.yaml" ]]
48+
[[ "$output" =~ "hello-kubernetes" ]]
49+
}
50+
51+
@test "Reformat JSON via stdin to junit format" {
52+
run bash -c "cat \"$DIR/test_results.json\" | $CONFTEST reformat --output junit"
53+
[ "$status" -eq 0 ]
54+
[[ "$output" =~ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" ]]
55+
[[ "$output" =~ "<testsuites>" ]]
56+
[[ "$output" =~ "hello-kubernetes" ]]
57+
}
58+
59+
@test "Handle empty stdin gracefully" {
60+
run bash -c "echo '' | $CONFTEST reformat --output table"
61+
[ "$status" -ne 0 ]
62+
[[ "$output" =~ "failed to parse JSON input" ]]
63+
}
64+
65+
@test "Handle malformed JSON via stdin" {
66+
run bash -c "echo 'invalid json' | $CONFTEST reformat --output table"
67+
[ "$status" -ne 0 ]
68+
[[ "$output" =~ "failed to parse JSON input" ]]
69+
}
70+
71+
@test "Fail when input file does not exist" {
72+
run $CONFTEST reformat nonexistent.json --output table
73+
[ "$status" -eq 1 ]
74+
[[ "$output" =~ "failed to open input file" ]]
75+
}
76+
77+
@test "Fail when invalid JSON provided" {
78+
echo "invalid json" > invalid.json
79+
run $CONFTEST reformat invalid.json --output table
80+
[ "$status" -eq 1 ]
81+
[[ "$output" =~ "failed to parse JSON input" ]]
82+
rm -f invalid.json
83+
}
84+
85+
@test "Handle invalid output format gracefully" {
86+
run $CONFTEST reformat "$DIR/test_results.json" --output invalidformat
87+
[ "$status" -eq 0 ]
88+
# Invalid format defaults to standard output format
89+
[[ "$output" =~ "hello-kubernetes" ]]
90+
}
91+
92+
@test "Handle empty JSON array" {
93+
echo '[]' > "$DIR/empty.json"
94+
run $CONFTEST reformat "$DIR/empty.json" --output table
95+
[ "$status" -eq 0 ]
96+
# Empty array should produce empty table output
97+
[[ ! "$output" =~ "hello-kubernetes" ]]
98+
}

0 commit comments

Comments
 (0)