Skip to content

Commit 59be019

Browse files
committed
Write output to a file
Refactor the output to support writing to a file This is to support the same functionality as validate image Example --output json=report.json
1 parent 47e7840 commit 59be019

File tree

1 file changed

+209
-38
lines changed

1 file changed

+209
-38
lines changed

cmd/validate/vsa.go

Lines changed: 209 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"sigs.k8s.io/yaml"
3838

3939
"github.com/conforma/cli/internal/applicationsnapshot"
40+
"github.com/conforma/cli/internal/format"
4041
"github.com/conforma/cli/internal/image"
4142
"github.com/conforma/cli/internal/output"
4243
"github.com/conforma/cli/internal/policy"
@@ -215,7 +216,14 @@ func addVSAFlags(cmd *cobra.Command, data *validateVSAData) {
215216
cmd.Flags().BoolVar(&data.noFallback, "no-fallback", false, "Disable fallback to image validation when VSA validation fails (fallback is enabled by default)")
216217
cmd.Flags().StringVar(&data.fallbackPublicKey, "fallback-public-key", "", "Public key to use for fallback image validation (different from VSA verification key)")
217218
// Output options
218-
cmd.Flags().StringSliceVar(&data.output, "output", []string{}, "Output formats (e.g., json, yaml, text)")
219+
validOutputFormats := []string{"json", "yaml", "text"}
220+
cmd.Flags().StringSliceVar(&data.output, "output", []string{}, hd.Doc(`
221+
write output to a file in a specific format. Use empty string path for stdout.
222+
May be used multiple times. Possible formats are:
223+
`+strings.Join(validOutputFormats, ", ")+`. In following format and file path
224+
additional options can be provided in key=value form following the question
225+
mark (?) sign, for example: --output text=output.txt?show-successes=false
226+
`))
219227
cmd.Flags().BoolVar(&data.strict, "strict", DefaultStrictMode, "Exit with non-zero code if validation fails")
220228

221229
// Parallel processing options
@@ -600,6 +608,16 @@ type VSASectionsReport struct {
600608
PolicyDiff *PolicyDiffReport `json:"policy_diff,omitempty" yaml:"policy_diff,omitempty"`
601609
}
602610

611+
// VSAReport holds the combined VSA sections and fallback report
612+
// VSA sections are at the root level, with fallback as an additional field
613+
type VSAReport struct {
614+
Header HeaderReport `json:"header" yaml:"header"`
615+
Result ResultReport `json:"result" yaml:"result"`
616+
VSASummary VSASummaryReport `json:"vsa_summary" yaml:"vsa_summary"`
617+
PolicyDiff *PolicyDiffReport `json:"policy_diff,omitempty" yaml:"policy_diff,omitempty"`
618+
Fallback *applicationsnapshot.Report `json:"fallback,omitempty" yaml:"fallback,omitempty"`
619+
}
620+
603621
// HeaderReport is the serializable version of HeaderDisplay
604622
type HeaderReport struct {
605623
Title string `json:"title" yaml:"title"`
@@ -1073,6 +1091,31 @@ func buildFallbackReportData(fallbackResults []validate_utils.Result, vsaData *v
10731091
}, nil
10741092
}
10751093

1094+
// createFallbackReport creates a fallback report without writing it
1095+
// This is used when we need to combine VSA and fallback output into a single file
1096+
func createFallbackReport(allData AllSectionsData, vsaData *validateVSAData) (*applicationsnapshot.Report, error) {
1097+
reportData, err := buildFallbackReportData(allData.FallbackResults, vsaData)
1098+
if err != nil {
1099+
return nil, err
1100+
}
1101+
1102+
// Create the report without writing it (same logic as WriteReport but without the WriteAll call)
1103+
report, err := applicationsnapshot.NewReport(
1104+
reportData.Snapshot,
1105+
reportData.Components,
1106+
reportData.Policy,
1107+
reportData.PolicyInputs,
1108+
reportData.ShowSuccesses,
1109+
reportData.ShowWarnings,
1110+
reportData.Expansion,
1111+
)
1112+
if err != nil {
1113+
return nil, err
1114+
}
1115+
1116+
return &report, nil
1117+
}
1118+
10761119
// displayFallbackImageSection - Displays fallback validate image output using WriteReport
10771120
func displayFallbackImageSection(allData AllSectionsData, vsaData *validateVSAData, cmd *cobra.Command) error {
10781121
// Only print header for text output format (or when no format is specified, which defaults to text)
@@ -1194,42 +1237,131 @@ func (d ComponentResultsDisplay) WriteAll(outputFormats []string, fs afero.Fs, c
11941237

11951238
for _, outputSpec := range outputFormats {
11961239
parts := strings.SplitN(outputSpec, "=", 2)
1197-
format := parts[0]
1240+
formatName := parts[0]
11981241
file := ""
11991242
if len(parts) > 1 {
12001243
file = parts[1]
12011244
}
12021245

1203-
data, err := d.toFormat(format)
1246+
data, err := d.toFormat(formatName)
12041247
if err != nil {
1205-
return fmt.Errorf("failed to convert to %s: %w", format, err)
1248+
return fmt.Errorf("failed to convert to %s: %w", formatName, err)
1249+
}
1250+
1251+
// Use shared file writing logic
1252+
if err := writeDataToOutput(data, formatName, file, fs, cmd); err != nil {
1253+
return err
1254+
}
1255+
}
1256+
1257+
return nil
1258+
}
1259+
1260+
// writeVSAReport writes a combined VSAReport to all specified output formats
1261+
// display is used for text format to avoid reconstructing ComponentResultsDisplay
1262+
func writeVSAReport(report VSAReport, display ComponentResultsDisplay, outputFormats []string, fs afero.Fs, cmd *cobra.Command) error {
1263+
if len(outputFormats) == 0 {
1264+
outputFormats = []string{"text"}
1265+
}
1266+
1267+
for _, outputSpec := range outputFormats {
1268+
parts := strings.SplitN(outputSpec, "=", 2)
1269+
formatName := parts[0]
1270+
file := ""
1271+
if len(parts) > 1 {
1272+
file = parts[1]
12061273
}
12071274

1208-
var writer io.Writer = cmd.OutOrStdout()
1209-
if file != "" {
1210-
f, err := fs.Create(file)
1275+
var data []byte
1276+
var err error
1277+
1278+
switch formatName {
1279+
case "json":
1280+
data, err = json.MarshalIndent(&report, "", " ")
1281+
if err != nil {
1282+
return fmt.Errorf("failed to marshal VSA report to JSON: %w", err)
1283+
}
1284+
case "yaml":
1285+
data, err = yaml.Marshal(&report)
12111286
if err != nil {
1212-
return fmt.Errorf("failed to create output file %s: %w", file, err)
1287+
return fmt.Errorf("failed to marshal VSA report to YAML: %w", err)
1288+
}
1289+
case "text":
1290+
// For text format, write VSA sections first, then fallback if present
1291+
var b strings.Builder
1292+
b.Write(display.toText())
1293+
1294+
// Write fallback section if present
1295+
if report.Fallback != nil {
1296+
b.WriteString("\n=== FALLBACK: VALIDATE IMAGE ===\n")
1297+
// Capture fallback text output using WriteAll with a string writer
1298+
formatOpts := format.Options{
1299+
ShowSuccesses: report.Fallback.ShowSuccesses,
1300+
ShowWarnings: report.Fallback.ShowWarnings,
1301+
}
1302+
var fallbackBuf strings.Builder
1303+
fallbackWriter := &stringWriter{&fallbackBuf}
1304+
fallbackParser := format.NewTargetParser(
1305+
applicationsnapshot.Text,
1306+
formatOpts,
1307+
fallbackWriter,
1308+
fs,
1309+
)
1310+
if err := report.Fallback.WriteAll([]string{"text"}, fallbackParser); err != nil {
1311+
return fmt.Errorf("failed to write fallback text: %w", err)
1312+
}
1313+
b.WriteString(fallbackBuf.String())
12131314
}
1214-
defer f.Close()
1215-
writer = f
1315+
data = []byte(b.String())
1316+
default:
1317+
return fmt.Errorf("unsupported format: %s (supported: json, yaml, text)", formatName)
12161318
}
12171319

1218-
if _, err := writer.Write(data); err != nil {
1219-
return fmt.Errorf("failed to write %s output: %w", format, err)
1320+
// Use shared file writing logic
1321+
if err := writeDataToOutput(data, formatName, file, fs, cmd); err != nil {
1322+
return err
12201323
}
1324+
}
12211325

1222-
// Add newline if not present
1223-
if !strings.HasSuffix(string(data), "\n") {
1224-
if _, err := writer.Write([]byte("\n")); err != nil {
1225-
return fmt.Errorf("failed to write newline: %w", err)
1226-
}
1326+
return nil
1327+
}
1328+
1329+
// writeDataToOutput writes data to stdout or a file, adding a newline if needed
1330+
// This is shared between ComponentResultsDisplay.WriteAll and writeVSAReport
1331+
func writeDataToOutput(data []byte, formatName, file string, fs afero.Fs, cmd *cobra.Command) error {
1332+
var writer io.Writer = cmd.OutOrStdout()
1333+
if file != "" {
1334+
f, err := fs.Create(file)
1335+
if err != nil {
1336+
return fmt.Errorf("failed to create output file %s: %w", file, err)
1337+
}
1338+
defer f.Close()
1339+
writer = f
1340+
}
1341+
1342+
if _, err := writer.Write(data); err != nil {
1343+
return fmt.Errorf("failed to write %s output: %w", formatName, err)
1344+
}
1345+
1346+
// Add newline if not present
1347+
if !strings.HasSuffix(string(data), "\n") {
1348+
if _, err := writer.Write([]byte("\n")); err != nil {
1349+
return fmt.Errorf("failed to write newline: %w", err)
12271350
}
12281351
}
12291352

12301353
return nil
12311354
}
12321355

1356+
// stringWriter is a helper to capture text output from WriteAll
1357+
type stringWriter struct {
1358+
buf *strings.Builder
1359+
}
1360+
1361+
func (w *stringWriter) Write(p []byte) (n int, err error) {
1362+
return w.buf.Write(p)
1363+
}
1364+
12331365
// displayComponentResults displays the validation results for each component
12341366
// Uses the new modular section-based approach
12351367
// Supports yaml, json, and text output formats based on the output flag
@@ -1249,36 +1381,65 @@ func displayComponentResults(allResults []vsa.ComponentResult, data *validateVSA
12491381
// Filter output formats to only json, yaml, text (exclude other formats like appstudio, etc.)
12501382
vsaOutputFormats := filterVSAOutputFormats(data.output)
12511383

1252-
if len(vsaOutputFormats) > 0 {
1253-
// Use format system for structured output (json/yaml/text)
1384+
// Check if we need combined output (file output specified AND fallback will be used)
1385+
hasFileOutput := hasFileOutputInFormats(vsaOutputFormats)
1386+
1387+
// If file output is specified and fallback will be used, combine them
1388+
if hasFileOutput && allData.FallbackUsed {
1389+
// Create fallback report without writing
1390+
fallbackReport, err := createFallbackReport(allData, data)
1391+
if err != nil {
1392+
return fmt.Errorf("failed to create fallback report: %w", err)
1393+
}
1394+
1395+
// Build combined VSAReport (call toVSASectionsReport once and reuse)
1396+
vsaSections := display.toVSASectionsReport()
1397+
vsaReport := VSAReport{
1398+
Header: vsaSections.Header,
1399+
Result: vsaSections.Result,
1400+
VSASummary: vsaSections.VSASummary,
1401+
PolicyDiff: vsaSections.PolicyDiff,
1402+
Fallback: fallbackReport,
1403+
}
1404+
1405+
// Write combined report to file(s)
12541406
fs := utils.FS(cmd.Context())
1255-
if err := display.WriteAll(vsaOutputFormats, fs, cmd); err != nil {
1256-
return fmt.Errorf("failed to write VSA sections output: %w", err)
1407+
if err := writeVSAReport(vsaReport, display, vsaOutputFormats, fs, cmd); err != nil {
1408+
return fmt.Errorf("failed to write combined VSA report: %w", err)
12571409
}
12581410
} else {
1259-
// Default: display as text to stdout using existing String() methods
1260-
// This reuses the same formatting logic as the structured output
1261-
fmt.Print(display.Header.String())
1262-
fmt.Println()
1263-
1264-
fmt.Print(display.Result.String())
1265-
fmt.Println()
1411+
// Use separate output (current behavior)
1412+
if len(vsaOutputFormats) > 0 {
1413+
// Use format system for structured output (json/yaml/text)
1414+
fs := utils.FS(cmd.Context())
1415+
if err := display.WriteAll(vsaOutputFormats, fs, cmd); err != nil {
1416+
return fmt.Errorf("failed to write VSA sections output: %w", err)
1417+
}
1418+
} else {
1419+
// Default: display as text to stdout using existing String() methods
1420+
// This reuses the same formatting logic as the structured output
1421+
fmt.Print(display.Header.String())
1422+
fmt.Println()
12661423

1267-
fmt.Print(display.VSASummary.String())
1268-
fmt.Println()
1424+
fmt.Print(display.Result.String())
1425+
fmt.Println()
12691426

1270-
// Conditional sections
1271-
if display.PolicyDiff != nil {
1272-
fmt.Print(display.PolicyDiff.String())
1427+
fmt.Print(display.VSASummary.String())
12731428
fmt.Println()
1429+
1430+
// Conditional sections
1431+
if display.PolicyDiff != nil {
1432+
fmt.Print(display.PolicyDiff.String())
1433+
fmt.Println()
1434+
}
12741435
}
1275-
}
12761436

1277-
if allData.FallbackUsed {
1278-
if err := displayFallbackImageSection(allData, data, cmd); err != nil {
1279-
return err
1437+
if allData.FallbackUsed {
1438+
if err := displayFallbackImageSection(allData, data, cmd); err != nil {
1439+
return err
1440+
}
1441+
fmt.Println()
12801442
}
1281-
fmt.Println()
12821443
}
12831444

12841445
return nil
@@ -1298,6 +1459,16 @@ func filterVSAOutputFormats(outputFormats []string) []string {
12981459
return filtered
12991460
}
13001461

1462+
// hasFileOutputInFormats checks if any format in the list specifies a file path
1463+
func hasFileOutputInFormats(outputFormats []string) bool {
1464+
for _, format := range outputFormats {
1465+
if strings.Contains(format, "=") {
1466+
return true
1467+
}
1468+
}
1469+
return false
1470+
}
1471+
13011472
// isFailureResult determines if a result represents a failure
13021473
func isFailureResult(result vsa.ComponentResult) bool {
13031474
resultType := classifyResult(result)

0 commit comments

Comments
 (0)