Conversation
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
There was a problem hiding this comment.
Pull request overview
Adds machine-readable (JSON/YAML) output for crossplane-diff comp so composition diffs and their downstream XR impacts can be consumed programmatically (e.g., CI/CD).
Changes:
- Introduces internal + JSON/YAML output types for composition diff results, including XR impact status and downstream changes.
- Adds human-readable and structured composition diff renderers, plus unit tests covering JSON/YAML schema and formatting helpers.
- Refactors the composition diff processor to build a structured result and delegate rendering via a new renderer factory based on
--output.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/diff/renderer/structured_renderer.go | Adds internal comp-diff result model + JSON output schema types and conversion helpers. |
| cmd/diff/renderer/comp_diff_renderer.go | New human/structured comp diff renderers; structured renderer emits JSON/YAML. |
| cmd/diff/renderer/comp_diff_renderer_test.go | New tests for structured comp diff output + formatting helpers. |
| cmd/diff/diffprocessor/processor_config.go | Adds CompDiffRenderer factory and wires it to --output selection. |
| cmd/diff/diffprocessor/comp_processor.go | Refactors comp diff processing to build CompDiffOutput and render via renderer. |
| cmd/diff/diffprocessor/comp_processor_test.go | Updates tests to use constructor wiring and new method signatures. |
| README.md | Documents `--output json |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // OutputError represents an error in structured output. | ||
| // Used consistently by both XR diff and comp diff for machine-readable error handling. | ||
| type OutputError struct { | ||
| ResourceID string `json:"resourceID,omitempty" yaml:"resourceId,omitempty"` |
There was a problem hiding this comment.
OutputError.ResourceID uses json:"resourceID" but yaml:"resourceId", and the repo docs (CLAUDE.md) describe resourceId. This makes structured output inconsistent across JSON/YAML and likely breaks consumers expecting a stable field name. Use a single canonical key (e.g., resourceId) for both JSON and YAML tags, and keep it consistent with the documented schema.
| ResourceID string `json:"resourceID,omitempty" yaml:"resourceId,omitempty"` | |
| ResourceID string `json:"resourceId,omitempty" yaml:"resourceId,omitempty"` |
| type xrImpactJSON struct { | ||
| corev1.ObjectReference `json:",inline"` | ||
|
|
||
| Status XRStatus `json:"status" yaml:"status"` | ||
| Error string `json:"error,omitempty" yaml:"error,omitempty"` | ||
| DownstreamChanges *DownstreamChanges `json:"downstreamChanges,omitempty" yaml:"downstreamChanges,omitempty"` |
There was a problem hiding this comment.
corev1.ObjectReference is embedded with the struct tag json:",inline", but encoding/json does not support inline and the presence of a json tag prevents anonymous-field promotion. This will likely serialize as a nested ObjectReference:{...} field instead of top-level apiVersion/kind/name/namespace, conflicting with the README example and intended machine-readable schema. Embed corev1.ObjectReference without a json tag (and, if needed, use yaml:",inline" for YAML) so identity fields are actually inlined.
| validate: func(t *testing.T, result string) { | ||
| t.Helper() | ||
|
|
||
| var parsed compDiffJSONOutput | ||
| if err := json.Unmarshal([]byte(result), &parsed); err != nil { | ||
| t.Fatalf("Failed to parse JSON: %v", err) | ||
| } | ||
|
|
||
| if len(parsed.Compositions) != 1 { | ||
| t.Fatalf("Expected 1 composition, got %d", len(parsed.Compositions)) | ||
| } | ||
|
|
||
| if parsed.Compositions[0].Name != "test-comp" { | ||
| t.Errorf("Expected name 'test-comp', got '%s'", parsed.Compositions[0].Name) | ||
| } | ||
|
|
||
| if parsed.Compositions[0].AffectedResources.Total != 2 { | ||
| t.Errorf("Expected total 2, got %d", parsed.Compositions[0].AffectedResources.Total) | ||
| } | ||
| // Verify compositionChanges is present in JSON | ||
| if parsed.Compositions[0].CompositionChanges == nil { | ||
| t.Error("Expected compositionChanges to be present") | ||
| } | ||
| }, |
There was a problem hiding this comment.
The structured comp diff tests don't currently assert the JSON/YAML shape of the embedded XR identity fields (apiVersion/kind/name/namespace). Adding an assertion that these fields are top-level (and not nested under an ObjectReference key) would catch schema regressions like incorrect struct tags on the embedded reference.
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| t.Error("Expected Manual update policy message") | ||
| } | ||
| }, | ||
| }, |
There was a problem hiding this comment.
DefaultCompDiffRenderer now has an error-bearing path via CompositionDiff.Error (set by the processor), but the unit tests here don't cover how the renderer should behave when a composition failed to process. Adding a test case with CompositionDiff{Error: ...} would help prevent regressions and ensure error output is user-friendly.
| }, | |
| }, | |
| "CompositionProcessingError": { | |
| output: &CompDiffOutput{ | |
| Compositions: []CompositionDiff{{ | |
| Name: "error-comp", | |
| Error: errors.New("failed to process composition"), | |
| }}, | |
| }, | |
| colorize: false, | |
| validate: func(t *testing.T, result string) { | |
| t.Helper() | |
| if !strings.Contains(result, "error-comp") { | |
| t.Error("Expected output to contain the composition name for error case") | |
| } | |
| if !strings.Contains(result, "failed to process composition") { | |
| t.Error("Expected output to include the composition error message") | |
| } | |
| }, | |
| }, |
|
|
||
| // Write errors at the end (for human-readable output) | ||
| for _, e := range errs { | ||
| errMsg := fmt.Sprintf("ERROR: %s: %s", e.ResourceID, e.Message) |
There was a problem hiding this comment.
DefaultDiffRenderer prints errors as ERROR: %s: %s unconditionally. Since OutputError.ResourceID is optional, this can yield ERROR: : <message> when ResourceID is empty. Consider omitting the extra colon/prefix when ResourceID isn't set (or using a different format for global errors).
| errMsg := fmt.Sprintf("ERROR: %s: %s", e.ResourceID, e.Message) | |
| var errMsg string | |
| if e.ResourceID != "" { | |
| errMsg = fmt.Sprintf("ERROR: %s: %s", e.ResourceID, e.Message) | |
| } else { | |
| errMsg = fmt.Sprintf("ERROR: %s", e.Message) | |
| } |
| comps := rawParsed["compositions"].([]any) | ||
| comp := comps[0].(map[string]any) | ||
| impacts := comp["impactAnalysis"].([]any) | ||
| impact := impacts[0].(map[string]any) | ||
|
|
There was a problem hiding this comment.
This test relies on unchecked type assertions (e.g., rawParsed["compositions"].([]any)), which will panic if the JSON shape changes and can obscure the actual failure. Consider using comma-ok assertions and failing the test with a descriptive message instead of panicking.
| comps := rawParsed["compositions"].([]any) | |
| comp := comps[0].(map[string]any) | |
| impacts := comp["impactAnalysis"].([]any) | |
| impact := impacts[0].(map[string]any) | |
| rawComps, ok := rawParsed["compositions"] | |
| if !ok { | |
| t.Fatalf("Expected 'compositions' key in JSON output, but it was missing") | |
| } | |
| comps, ok := rawComps.([]any) | |
| if !ok { | |
| t.Fatalf("Expected 'compositions' to be a JSON array, got %T", rawComps) | |
| } | |
| if len(comps) == 0 { | |
| t.Fatalf("Expected at least one composition in 'compositions', but got none") | |
| } | |
| comp, ok := comps[0].(map[string]any) | |
| if !ok { | |
| t.Fatalf("Expected first element of 'compositions' to be an object, got %T", comps[0]) | |
| } | |
| rawImpacts, ok := comp["impactAnalysis"] | |
| if !ok { | |
| t.Fatalf("Expected 'impactAnalysis' key in first composition, but it was missing") | |
| } | |
| impacts, ok := rawImpacts.([]any) | |
| if !ok { | |
| t.Fatalf("Expected 'impactAnalysis' to be a JSON array, got %T", rawImpacts) | |
| } | |
| if len(impacts) == 0 { | |
| t.Fatalf("Expected at least one impact in 'impactAnalysis', but got none") | |
| } | |
| impact, ok := impacts[0].(map[string]any) | |
| if !ok { | |
| t.Fatalf("Expected first element of 'impactAnalysis' to be an object, got %T", impacts[0]) | |
| } |
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
Signed-off-by: Jonathan Ogilvie <jonathan.ogilvie@sumologic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Description of your changes
Add machine-readable output for
compdiff.I have:
earthly -P +reviewableto ensure this PR is ready for review.- [ ] Followed the API promotion workflow if this PR introduces, removes, or promotes an API.Need help with this checklist? See the cheat sheet.