Repository: project-copacetic/copacetic (go)

Issue:
[REQ] Multi Platform Summary Enhancement
### What kind of request is this?

None

### What is your request or suggestion?

Change "ERROR" field in multi arch summary report to "MESSAGE".  This can specify why the image was not patched. For example "No scanner report", "Windows image", or an error message if there was an error during patch.

### Are you willing to submit PRs to contribute to this feature request?

- [ ] Yes, I am willing to implement it.

Pull request:
feat: enhance multi platform summary report
Updated ERROR column naming to MESSAGE.
Also added the "No windows supported" error

Currently if we don't provide reportFile for an image under the mutliArch patching it doesn't return an error and just logs a WARN message about no VEX Report creation (through singleArchPatch) and continues patching in the update all method.
I added a "No Scanner Report" error where the patching for that image will be ignored and we'll show that image with "No scanner report" in the tabular summary.

Closes #1122 


Patch:
diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go
index de14e91d..af6129bf 100644
--- a/pkg/patch/patch.go
+++ b/pkg/patch/patch.go
@@ -759,24 +759,55 @@ func patchMultiPlatformImage(
 				// No report for this platform - preserve original
 				log.Infof("No report for platform %s, preserving original in manifest", p.OS+"/"+p.Architecture)
 
+				// Parse the original image reference for the result
+				originalRef, err := reference.ParseNormalizedNamed(image)
+				if err != nil {
+					mu.Lock()
+					summaryMap[platformKey] = &types.MultiPlatformSummary{
+						Platform: platformKey,
+						Status:   "Error",
+						Ref:      "",
+						Message:  fmt.Sprintf("failed to parse original image reference: %v", err),
+					}
+					mu.Unlock()
+					return err
+				}
+
 				// Handle Windows platform without push enabled
 				if !push && p.OS == "windows" {
+					mu.Lock()
+					defer mu.Unlock()
 					if !ignoreError {
+						summaryMap[platformKey] = &types.MultiPlatformSummary{
+							Platform: platformKey,
+							Status:   "Error",
+							Ref:      originalRef.String() + " (original reference)",
+							Message:  "Windows images are not patched",
+						}
 						return errors.New("cannot save Windows platform image without pushing to registry. Use --push flag to save Windows images to a registry or run with --ignore-errors")
 					}
+					summaryMap[platformKey] = &types.MultiPlatformSummary{
+						Platform: platformKey,
+						Status:   "Ignored",
+						Ref:      originalRef.String() + " (original reference)",
+						Message:  "Windows images are not patched and will be preserved as-is",
+					}
 					log.Warn("Cannot save Windows platform image without pushing to registry. Use --push flag to save Windows images to a registry.")
+					return nil
 				}
 
 				// Get the original platform descriptor from the manifest
 				originalDesc, err := getPlatformDescriptorFromManifest(image, &p)
 				if err != nil {
-					return fmt.Errorf("failed to get original descriptor for platform %s: %w", p.OS+"/"+p.Architecture, err)
-				}
-
-				// Parse the original image reference for the result
-				originalRef, err := reference.ParseNormalizedNamed(image)
-				if err != nil {
-					return fmt.Errorf("failed to parse original image reference: %w", err)
+					mu.Lock()
+					summaryMap[platformKey] = &types.MultiPlatformSummary{
+						Platform: platformKey,
+						Status:   "Error",
+						Ref:      "",
+						Message:  fmt.Sprintf("failed to get original descriptor for platform %s: %v", p.OS+"/"+p.Architecture, err),
+					}
+					mu.Unlock()
+					return err
 				}
 
 				// For platforms without reports, use the original image digest/reference
@@ -793,10 +824,9 @@ func patchMultiPlatformImage(
 					Platform: platformKey,
 					Status:   "Not Patched",
 					Ref:      originalRef.String() + " (original reference)",
-					Error:    "",
+					Message:  "Preserved original image (No Scan Report provided for platform)",
 				}
 				mu.Unlock()
-				log.Infof("Preserved original image (%s): %s\n", p.OS+"/"+p.Architecture, originalRef.String())
 				return nil
 			}
 
@@ -812,7 +842,7 @@ func patchMultiPlatformImage(
 					Platform: platformKey,
 					Status:   status,
 					Ref:      "",
-					Error:    err.Error(),
+					Message:  err.Error(),
 				}
 				if !ignoreError {
 					return err
@@ -823,7 +853,7 @@ func patchMultiPlatformImage(
 					Platform: platformKey,
 					Status:   "Error",
 					Ref:      "",
-					Error:    "patchSingleArchImage returned nil result",
+					Message:  "patchSingleArchImage returned nil result",
 				}
 				return nil
 			}
@@ -833,9 +863,8 @@ func patchMultiPlatformImage(
 				Platform: platformKey,
 				Status:   "Patched",
 				Ref:      res.PatchedRef.String(),
-				Error:    "",
+				Message:  fmt.Sprintf("Successfully patched image (%s)", p.OS+"/"+p.Architecture),
 			}
-			log.Infof("Patched image (%s): %s\n", p.OS+"/"+p.Architecture, res.PatchedRef.String())
 			return nil
 		})
 	}
@@ -907,7 +936,7 @@ func patchMultiPlatformImage(
 
 	var b strings.Builder
 	w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0)
-	fmt.Fprintln(w, "PLATFORM\tSTATUS\tREFERENCE\tERROR")
+	fmt.Fprintln(w, "PLATFORM\tSTATUS\tREFERENCE\tMESSAGE")
 
 	for _, p := range platforms {
 		platformKey := buildkit.PlatformKey(p.Platform)
@@ -917,7 +946,7 @@ func patchMultiPlatformImage(
 			if ref == "" {
 				ref = "-"
 			}
-			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", s.Platform, s.Status, ref, s.Error)
+			fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", s.Platform, s.Status, ref, s.Message)
 		}
 	}
 	w.Flush()
diff --git a/pkg/types/types.go b/pkg/types/types.go
index efc7d685..2b0c7329 100644
--- a/pkg/types/types.go
+++ b/pkg/types/types.go
@@ -46,5 +46,5 @@ type MultiPlatformSummary struct {
 	Platform string
 	Status   string
 	Ref      string
-	Error    string
+	Message  string
 }


Test patch:
diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go
index d308c067..3ebb0e67 100644
--- a/pkg/patch/patch_test.go
+++ b/pkg/patch/patch_test.go
@@ -591,6 +591,7 @@ func TestMultiPlatformSummaryTable(t *testing.T) {
 		{"linux", "amd64", ""},
 		{"linux", ARM64, ""},
 		{"linux", "arm", "v7"},
+		{"windows", "amd64", ""},
 	}
 
 	summaryMap := map[string]*types.MultiPlatformSummary{
@@ -598,25 +599,31 @@ func TestMultiPlatformSummaryTable(t *testing.T) {
 			Platform: "linux/amd64",
 			Status:   "Patched",
 			Ref:      "docker.io/library/nginx:patched-amd64",
-			Error:    "",
+			Message:  "",
 		},
 		"linux/arm64": {
 			Platform: "linux/arm64",
 			Status:   "Error",
 			Ref:      "",
-			Error:    "emulation is not enabled for platform linux/arm64",
+			Message:  "emulation is not enabled for platform linux/arm64",
 		},
 		"linux/arm/v7": {
 			Platform: "linux/arm/v7",
 			Status:   "Ignored",
 			Ref:      "",
-			Error:    "",
+			Message:  "",
+		},
+		"windows/amd64": {
+			Platform: "windows/amd64",
+			Status:   "Not Patched",
+			Ref:      "docker.io/library/nginx (original reference)",
+			Message:  "Windows Image (Original Preserved)",
 		},
 	}
 
 	var b strings.Builder
 	w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0)
-	_, _ = w.Write([]byte("PLATFORM\tSTATUS\tREFERENCE\tERROR\n"))
+	_, _ = w.Write([]byte("PLATFORM\tSTATUS\tREFERENCE\tMESSAGE\n"))
 	for _, p := range platforms {
 		platformKey := buildkit.PlatformKey(ispec.Platform{
 			OS:           p.OS,
@@ -630,20 +637,27 @@ func TestMultiPlatformSummaryTable(t *testing.T) {
 				ref = "-"
 			}
 			_, _ = w.Write([]byte(
-				s.Platform + "\t" + s.Status + "\t" + ref + "\t" + s.Error + "\n",
+				s.Platform + "\t" + s.Status + "\t" + ref + "\t" + s.Message + "\n",
 			))
 		}
 	}
 	w.Flush()
 
 	got := b.String()
-	expected := `PLATFORM      STATUS   REFERENCE                              ERROR
-linux/amd64   Patched  docker.io/library/nginx:patched-amd64  
-linux/arm64   Error    -                                      emulation is not enabled for platform linux/arm64
-linux/arm/v7  Ignored  -                                      
+	expected := `PLATFORM       STATUS       REFERENCE                                     MESSAGE
+linux/amd64    Patched      docker.io/library/nginx:patched-amd64
+linux/arm64    Error        -                                             emulation is not enabled for platform linux/arm64
+linux/arm/v7   Ignored      -
+windows/amd64  Not Patched  docker.io/library/nginx (original reference)  Windows Image (Original Preserved)
 `
-
-	if got != expected {
-		t.Errorf("summary table output mismatch:\ngot:\n%s\nwant:\n%s", got, expected)
+	gotLines := strings.FieldsFunc(got, func(r rune) bool { return r == '\n' || r == '\r' })
+	expectedLines := strings.FieldsFunc(expected, func(r rune) bool { return r == '\n' || r == '\r' })
+	if len(gotLines) != len(expectedLines) {
+		t.Errorf("line count mismatch:\ngot:\n%s\nwant:\n%s", got, expected)
+	}
+	for i := range gotLines {
+		if strings.TrimSpace(gotLines[i]) != strings.TrimSpace(expectedLines[i]) {
+			t.Errorf("line %d mismatch:\ngot:   %q\nwant:  %q", i+1, gotLines[i], expectedLines[i])
+		}
 	}
 }