Skip to content

Commit 720e163

Browse files
committed
Add support for --output text format to ec validate input command
bringing it to feature parity with ec validate image. The text format provides a human-readable, color-coded output that is easier to read than JSON. - Add text output format support with templates - Change default output from JSON to text (matching validate image) - Add ShowSuccesses/ShowWarnings flag support - Add comprehensive test coverage - Update existing tests for new API resolves: EC-1493
1 parent 454bb13 commit 720e163

File tree

5 files changed

+257
-11
lines changed

5 files changed

+257
-11
lines changed

cmd/validate/input.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
log "github.com/sirupsen/logrus"
2929
"github.com/spf13/cobra"
3030

31-
"github.com/conforma/cli/internal/applicationsnapshot"
3231
"github.com/conforma/cli/internal/format"
3332
"github.com/conforma/cli/internal/input"
3433
"github.com/conforma/cli/internal/output"
@@ -209,12 +208,12 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
209208
return inputs[i].FilePath > inputs[j].FilePath
210209
})
211210

212-
report, err := input.NewReport(inputs, data.policy, manyPolicyInput)
211+
report, err := input.NewReport(inputs, data.policy, manyPolicyInput, showSuccesses, showWarnings)
213212
if err != nil {
214213
return err
215214
}
216215

217-
p := format.NewTargetParser(input.JSON, format.Options{ShowSuccesses: showSuccesses, ShowWarnings: showWarnings}, cmd.OutOrStdout(), utils.FS(cmd.Context()))
216+
p := format.NewTargetParser(input.Text, format.Options{ShowSuccesses: showSuccesses, ShowWarnings: showWarnings}, cmd.OutOrStdout(), utils.FS(cmd.Context()))
218217
if err := report.WriteAll(data.output, p); err != nil {
219218
return err
220219
}
@@ -235,7 +234,7 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
235234
* git reference (github.com/user/repo//default?ref=main), or
236235
* inline JSON ('{sources: {...}}')")`))
237236

238-
validOutputFormats := applicationsnapshot.OutputFormats
237+
validOutputFormats := []string{input.JSON, input.YAML, input.Text, input.Summary}
239238
cmd.Flags().StringSliceVarP(&data.output, "output", "o", data.output, hd.Doc(`
240239
Write output to a file in a specific format, e.g. yaml=/tmp/output.yaml. Use empty string
241240
path for stdout, e.g. yaml. May be used multiple times. Possible formats are:

cmd/validate/input_test.go

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func Test_ValidateInputCmd_SuccessSingleFile(t *testing.T) {
8686
"input",
8787
"--file", "/input.yaml",
8888
"--policy", `{"publicKey": "testkey"}`,
89+
"--output", "json", // Explicitly request JSON output
8990
})
9091

9192
utils.SetTestRekorPublicKey(t)
@@ -132,6 +133,7 @@ func Test_ValidateInputCmd_SuccessMultipleFiles(t *testing.T) {
132133
"--file", "/input1.yaml",
133134
"--file", "/input2.yaml",
134135
"--policy", `{"name":"Default","description":"Stuff and things","sources":[{"name":"Default","policy":["/bacon/and/eggs/policy/lib","/bacon/and/eggs/policy/release"],"data":["/bacon/and/eggs/example/data"],"config":{"include":["sbom_cyclonedx"],"exclude":[]}}]}`,
136+
"--output", "json", // Explicitly request JSON output
135137
})
136138

137139
utils.SetTestRekorPublicKey(t)
@@ -359,7 +361,8 @@ func Test_ValidateInputCmd_ShowWarningsFlag(t *testing.T) {
359361
require.NoError(t, afero.WriteFile(fs, "/file.yaml", []byte("some: data"), 0644))
360362

361363
cmd, buf := setUpValidateInputCmd(warningValidator, fs)
362-
cmd.SetArgs(c.args)
364+
args := append(c.args, "--output", "json") // Explicitly request JSON output
365+
cmd.SetArgs(args)
363366

364367
utils.SetTestRekorPublicKey(t)
365368
err := cmd.Execute()
@@ -394,3 +397,87 @@ func Test_ValidateInputCmd_ShowWarningsFlag(t *testing.T) {
394397
})
395398
}
396399
}
400+
401+
func Test_ValidateInputCmd_DefaultTextOutput(t *testing.T) {
402+
fs := afero.NewMemMapFs()
403+
require.NoError(t, afero.WriteFile(fs, "/input.yaml", []byte("some: data"), 0644))
404+
405+
// Mock validator: returns success with no violations, one success result.
406+
outMock := &output.Output{
407+
PolicyCheck: []evaluator.Outcome{
408+
{
409+
Successes: []evaluator.Result{
410+
{
411+
Message: "Everything looks great!",
412+
Metadata: map[string]interface{}{
413+
"code": "policy.nice",
414+
},
415+
},
416+
},
417+
},
418+
},
419+
}
420+
421+
cmd, buf := setUpValidateInputCmd(mockValidate(outMock, nil), fs)
422+
cmd.SetArgs([]string{
423+
"input",
424+
"--file", "/input.yaml",
425+
"--policy", `{"publicKey": "testkey"}`,
426+
// No --output flag, should default to text
427+
})
428+
429+
utils.SetTestRekorPublicKey(t)
430+
err := cmd.Execute()
431+
assert.NoError(t, err)
432+
433+
output := buf.String()
434+
// Verify text output format
435+
assert.Contains(t, output, "Success: true")
436+
assert.Contains(t, output, "Result: SUCCESS")
437+
assert.Contains(t, output, "Violations: 0")
438+
assert.Contains(t, output, "Input File: /input.yaml")
439+
// Results section should not appear when there are only successes and showSuccesses=false
440+
assert.NotContains(t, output, "Results:")
441+
}
442+
443+
func Test_ValidateInputCmd_TextOutputWithShowSuccesses(t *testing.T) {
444+
fs := afero.NewMemMapFs()
445+
require.NoError(t, afero.WriteFile(fs, "/input.yaml", []byte("some: data"), 0644))
446+
447+
// Mock validator: returns success with one success result.
448+
outMock := &output.Output{
449+
PolicyCheck: []evaluator.Outcome{
450+
{
451+
Successes: []evaluator.Result{
452+
{
453+
Message: "Everything looks great!",
454+
Metadata: map[string]interface{}{
455+
"code": "policy.nice",
456+
},
457+
},
458+
},
459+
},
460+
},
461+
}
462+
463+
cmd, buf := setUpValidateInputCmd(mockValidate(outMock, nil), fs)
464+
cmd.SetArgs([]string{
465+
"input",
466+
"--file", "/input.yaml",
467+
"--policy", `{"publicKey": "testkey"}`,
468+
"--show-successes",
469+
})
470+
471+
utils.SetTestRekorPublicKey(t)
472+
err := cmd.Execute()
473+
assert.NoError(t, err)
474+
475+
output := buf.String()
476+
// Verify text output format with successes shown
477+
assert.Contains(t, output, "Success: true")
478+
assert.Contains(t, output, "Result: SUCCESS")
479+
assert.Contains(t, output, "Successes: 1")
480+
assert.Contains(t, output, "Input File: /input.yaml")
481+
assert.Contains(t, output, "Results:")
482+
assert.Contains(t, output, "[Success] policy.nice")
483+
}

docs/modules/ROOT/pages/ec_validate_input.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ violations, include the title and the description of the failed policy
5252
rule. (Default: false)
5353
-o, --output:: Write output to a file in a specific format, e.g. yaml=/tmp/output.yaml. Use empty string
5454
path for stdout, e.g. yaml. May be used multiple times. Possible formats are:
55-
json, yaml, text, appstudio, summary, summary-markdown, junit, attestation, policy-input, vsa. In following format and file path
55+
json, yaml, text, summary. In following format and file path
5656
additional options can be provided in key=value form following the question
5757
mark (?) sign, for example: --output text=output.txt?show-successes=false
5858
(Default: [])

internal/input/report.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package input
1818

1919
import (
2020
"bytes"
21+
"embed"
2122
"encoding/json"
2223
"errors"
2324
"fmt"
@@ -29,6 +30,7 @@ import (
2930
"github.com/conforma/cli/internal/evaluator"
3031
"github.com/conforma/cli/internal/format"
3132
"github.com/conforma/cli/internal/policy"
33+
"github.com/conforma/cli/internal/utils"
3234
"github.com/conforma/cli/internal/version"
3335
)
3436

@@ -50,6 +52,8 @@ type Report struct {
5052
Data any `json:"-"`
5153
EffectiveTime time.Time `json:"effective-time"`
5254
PolicyInput [][]byte `json:"-"`
55+
ShowSuccesses bool `json:"-"`
56+
ShowWarnings bool `json:"-"`
5357
}
5458

5559
type summary struct {
@@ -87,12 +91,13 @@ type TestReport struct {
8791
const (
8892
JSON = "json"
8993
YAML = "yaml"
94+
Text = "text"
9095
Summary = "summary"
9196
)
9297

9398
// WriteReport returns a new instance of Report representing the state of
9499
// the filepaths provided.
95-
func NewReport(inputs []Input, policy policy.Policy, policyInput [][]byte) (Report, error) {
100+
func NewReport(inputs []Input, policy policy.Policy, policyInput [][]byte, showSuccesses bool, showWarnings bool) (Report, error) {
96101
success := true
97102

98103
// Set the report success, remains true if all the files were successfully validated
@@ -113,13 +118,15 @@ func NewReport(inputs []Input, policy policy.Policy, policyInput [][]byte) (Repo
113118
EcVersion: info.Version,
114119
EffectiveTime: policy.EffectiveTime().UTC(),
115120
PolicyInput: policyInput,
121+
ShowSuccesses: showSuccesses,
122+
ShowWarnings: showWarnings,
116123
}, nil
117124
}
118125

119126
// WriteAll writes the report to all the given targets.
120127
func (r Report) WriteAll(targets []string, p format.TargetParser) (allErrors error) {
121128
if len(targets) == 0 {
122-
targets = append(targets, JSON)
129+
targets = append(targets, Text)
123130
}
124131
for _, targetName := range targets {
125132
target, err := p.Parse(targetName)
@@ -152,6 +159,8 @@ func (r *Report) toFormat(format string) (data []byte, err error) {
152159
data, err = json.Marshal(r)
153160
case YAML:
154161
data, err = yaml.Marshal(r)
162+
case Text:
163+
data, err = generateTextReport(r)
155164
case Summary:
156165
data, err = json.Marshal(r.toSummary())
157166
default:
@@ -206,3 +215,43 @@ func condensedMsg(results []evaluator.Result) map[string][]string {
206215
}
207216
return shortNames
208217
}
218+
219+
//go:embed templates/*.tmpl
220+
var efs embed.FS
221+
222+
func generateTextReport(r *Report) ([]byte, error) {
223+
// Prepare some template input
224+
// Calculate totals for the test report structure
225+
var successes, failures, warnings int
226+
for _, input := range r.FilePaths {
227+
successes += input.SuccessCount
228+
failures += len(input.Violations)
229+
warnings += len(input.Warnings)
230+
}
231+
232+
result := "SUCCESS"
233+
if failures > 0 {
234+
result = "FAILURE"
235+
} else if warnings > 0 {
236+
result = "WARNING"
237+
}
238+
239+
testReport := TestReport{
240+
Timestamp: r.created.Format(time.RFC3339),
241+
Namespace: "",
242+
Successes: successes,
243+
Failures: failures,
244+
Warnings: warnings,
245+
Result: result,
246+
}
247+
248+
input := struct {
249+
Report *Report
250+
TestReport TestReport
251+
}{
252+
Report: r,
253+
TestReport: testReport,
254+
}
255+
256+
return utils.RenderFromTemplatesWithMain(input, "text_report.tmpl", efs)
257+
}

0 commit comments

Comments
 (0)