- 
                Notifications
    You must be signed in to change notification settings 
- Fork 48
OCPCLOUD-3172: machinesetsync: refactor to a generalized differ which can work independent of types #382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| @chrischdi: This pull request references OCPCLOUD-3172 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.21.0" version, but no target version was set. In response to this: Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. | 
| Skipping CI for Draft Pull Request. | 
| [APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. 
Needs approval from an approver in each of these files:
 Approvers can indicate their approval by writing  | 
| WalkthroughReplaces ad-hoc map-based diffs with a structured pkg/util DiffResult across machine and machineset controllers, adds a platform-aware differ with providerSpec handling and deterministic output, removes go-test/deep, updates signatures and error propagation across comparison/update paths, and adds unit tests for the differ and status diffs. Changes
 Sequence Diagram(s)sequenceDiagram
  autonumber
  participant Reconciler
  participant Converter
  participant Differ as "Differ (pkg/util)"
  participant API as "K8s API"
  Reconciler->>Converter: build converted object (CAPI/MAPI)
  Reconciler->>Differ: diffResult, err = Diff(existing, converted)
  alt Diff error
    Differ-->>Reconciler: error
    Reconciler->>API: record/return error
  else Diff computed
    Differ-->>Reconciler: DiffResult (HasChanges?, HasSpec?, HasStatus?, HasProviderSpec?)
    alt HasSpecChanges or HasMetadataChanges or HasProviderSpecChanges
      Reconciler->>API: patch/update spec/metadata
    end
    alt HasStatusChanges
      Reconciler->>API: patch/update status (observe generation guards)
    end
  end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 
 Files/areas needing extra attention: 
 Poem
 Pre-merge checks and finishing touches✅ Passed checks (3 passed)
 ✨ Finishing touches
 🧪 Generate unit tests (beta)
 Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.5.0)Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions Comment  | 
| /test unit | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is great and a clear improvement. I have some follow-on ideas, but I would personally gladly merge this and iterate on it in the unlikely event it ever became worth it.
I shared a thought inline based on something similar we did in ORC with field indexers.
Not at all important, but I also feel that Unstructured is an internal implementation detail: e.g. the Diff method could be implemented with reflection and the signature would not change.
        
          
                pkg/util/sync.go
              
                Outdated
          
        
      | differ := UnstructuredDiffer[clusterv1.MachineSetStatus]{ | ||
| customDiff: []func(a clusterv1.MachineSetStatus, b clusterv1.MachineSetStatus) ([]string, error){ | ||
| func(a, b clusterv1.MachineSetStatus) ([]string, error) { | ||
| return compareCAPIMachineSetConditions(a.Conditions, b.Conditions), nil | ||
| }, | ||
| }, | ||
| ignoreFields: [][]string{{"conditions"}}, | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
differ := UnstructuredDiffer[clusterv1.MachineSetStatus]()
differ = WithCustomDiff(differ,
    "conditions",
    func(x *clusterv1.MachineSetStatus) []clusterv1.Condition {return x.conditions},
    compareCAPIConditions)
func compareCAPIConditions(a, b []clusterv1.Conditions) []string {
    ...
}WithCustomDiff unfortunately can't be a method on UnstructuredDiffer as it has an additional type argument([]clusterv1.Condition).
Advantages of this approach:
- ignoreField is a required argument of CustomDiff, removing a footgun
- compareCAPIConditions is now reusable by all types with CAPI conditions
Disadvantages:
- The argument to pull a field out is a bit ugly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignoreField is a required argument of CustomDiff, removing a footgun
Are there cases when we want to ignore fields without custom diffs no? Maybe around deprecated fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes there are (e.g. CAPI's conversion-data annotation).
However I'd combine it, but keeping the option to ignore paths completely:
	differ := NewUnstructuredDiffer(
		WithCustomDiff([]string{"conditions"}, func(a, b clusterv1.MachineSetStatus) ([]string, error) {
			return compareCAPIV1Beta1Conditions(a.Conditions, b.Conditions), nil
		}),
		WithCustomDiff([]string{"v1beta2", "conditions"}, func(a, b clusterv1.MachineSetStatus) ([]string, error) {
			return compareCAPIV1Beta2Conditions(
					ptr.Deref(a.V1Beta2, clusterv1.MachineSetV1Beta2Status{}).Conditions,
					ptr.Deref(b.V1Beta2, clusterv1.MachineSetV1Beta2Status{}).Conditions),
				nil
		}),
		WithIgnoreField[clusterv1.MachineSetStatus]("conditions", "bar"),
	)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored, don't have the "WithCustomDiff" anymore.
I now have an option for the conditions part instead.
        
          
                pkg/util/diff.go
              
                Outdated
          
        
      | "k8s.io/apimachinery/pkg/runtime" | ||
| ) | ||
|  | ||
| type UnstructuredDiffer[T any] struct { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is unstructured, why is it also generic? Does it make this simpler than using runtime.Object for the a, b arguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed just to differ. Should work for any struct.
        
          
                pkg/util/diff.go
              
                Outdated
          
        
      |  | ||
| diff := deep.Equal(unstructuredA, unstructuredB) | ||
| if len(diff) > 0 { | ||
| diffs["deep.Equal"] = diff | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the deep.Equal key? Does this get exposed to end users? Is this a magic word?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not there anymore :-)
        
          
                pkg/util/sync.go
              
                Outdated
          
        
      | // Maybe we can also have it this way: | ||
| // differ := NewUnstructuredDiffer( | ||
| // WithIgnoreField[clusterv1.MachineSetStatus]("conditions"), | ||
| // WithCustomDiff(func(a, b clusterv1.MachineSetStatus) ([]string, error) { | ||
| // return compareCAPIMachineSetConditions(a.Conditions, b.Conditions), nil | ||
| // }), | ||
| // ) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored :-)
        
          
                pkg/util/sync.go
              
                Outdated
          
        
      | differ := UnstructuredDiffer[clusterv1.MachineSetStatus]{ | ||
| customDiff: []func(a clusterv1.MachineSetStatus, b clusterv1.MachineSetStatus) ([]string, error){ | ||
| func(a, b clusterv1.MachineSetStatus) ([]string, error) { | ||
| return compareCAPIMachineSetConditions(a.Conditions, b.Conditions), nil | ||
| }, | ||
| }, | ||
| ignoreFields: [][]string{{"conditions"}}, | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignoreField is a required argument of CustomDiff, removing a footgun
Are there cases when we want to ignore fields without custom diffs no? Maybe around deprecated fields?
ac815fd    to
    f494b0c      
    Compare
  
    | @chrischdi: This pull request references OCPCLOUD-3172 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.21.0" version, but no target version was set. In response to this: 
 Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. | 
| // Compare metadata | ||
| if diffMetadata, err := util.ObjectMetaEqual(metadata1, metadata2); err != nil { | ||
| return nil, fmt.Errorf("failed to compare Cluster API Infrastructure machine metadata: %w", err) | ||
| } else if diffMetadata.Changed() { | ||
| diff[".metadata"] = diffMetadata.String() | ||
| } | ||
|  | ||
| // TODO: Evaluate if we want to add status comparison if needed in the future (e.g. for scale from zero capacity). | ||
| // Compare spec | ||
| if diffSpec, err := util.NewDiffer().Diff(spec1, spec2); err != nil { | ||
| return nil, fmt.Errorf("failed to compare Cluster API Infrastructure machine spec: %w", err) | ||
| } else if diffSpec.Changed() { | ||
| diff[".spec"] = diffSpec.String() | ||
| } | ||
|  | ||
| return diff, nil | ||
| default: | ||
| return nil, fmt.Errorf("%w: %s", errPlatformNotSupported, platform) | ||
| // Compare status | ||
| if diffStatus, err := util.NewDiffer(util.WithIgnoreConditionsLastTransitionTime()).Diff(status1, status2); err != nil { | ||
| return nil, fmt.Errorf("failed to compare Cluster API Infrastructure machine status: %w", err) | ||
| } else if diffStatus.Changed() { | ||
| diff[".status"] = diffStatus.String() | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking about doing this even more generic, as this is the same across the codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More generic as in abstracting each of the diff fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, updated the PR. We now don't have to distinct and manually do the diff for metadata, spec, status.
We always compare the whole object and can have all results:
...Diff(clusterv1.Machine, clusterv1.Machine)
:-)
| status1 = typedInfraMachineTemplate1.Status | ||
| status2 = typedinfraMachineTemplate2.Status | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: added status conversion here.
Let's see if we break something or not :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (1)
pkg/controllers/machinesetsync/machineset_sync_controller.go (1)
1346-1354: Return the correct OpenStack assertion error.In the OpenStack branch we still return
errAssertingCAPIIBMPowerVSMachineTemplateon a type mismatch. Callers that rely onerrors.Is(..., errAssertingCAPIOpenStackMachineTemplate)will now miss this case, breaking platform-specific handling. Please swap the constant to the OpenStack one for both checks in this branch.- typedInfraMachineTemplate1, ok := infraMachineTemplate1.(*openstackv1.OpenStackMachineTemplate) - if !ok { - return nil, errAssertingCAPIIBMPowerVSMachineTemplate - } + typedInfraMachineTemplate1, ok := infraMachineTemplate1.(*openstackv1.OpenStackMachineTemplate) + if !ok { + return nil, errAssertingCAPIOpenStackMachineTemplate + } typedinfraMachineTemplate2, ok := infraMachineTemplate2.(*openstackv1.OpenStackMachineTemplate) if !ok { return nil, errAssertingCAPIOpenStackMachineTemplate }
🧹 Nitpick comments (2)
pkg/util/sync_test.go (1)
332-341: Assert the string output even for unchanged diffs
Right now we skipgot.String()comparisons whentt.wantChangesis false, so a future regression whereChanged()returns false butString()leaks content would slide through. Consider moving theg.Expect(got.String()).To(Equal(tt.want))assertion outside the conditional (the happy-path cases already setwantto""), or duplicating it in theelse. That keeps the test guarding both facets of the contract.pkg/controllers/machinesync/machine_sync_controller.go (1)
1259-1277: Tighten the error message wording
These error strings still say “Cluster API Infrastructure machine …” even though this helper compares the top-level CAPI Machine. Tweaking the wording here (and in the similar spec/status branches) would make logs clearer when we do hit this path.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
📒 Files selected for processing (7)
- pkg/controllers/machinesetsync/machineset_sync_controller.go(6 hunks)
- pkg/controllers/machinesync/machine_sync_controller.go(3 hunks)
- pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go(5 hunks)
- pkg/util/diff.go(1 hunks)
- pkg/util/diff_test.go(1 hunks)
- pkg/util/sync.go(1 hunks)
- pkg/util/sync_test.go(1 hunks)
🔇 Additional comments (1)
pkg/util/diff_test.go (1)
78-115: Nice coverage for slice diffs
Exercising add/remove/change scenarios on list fields gives strong confidence that the new differ is handling positional data correctly. Thanks for including these cases.
| unstructuredA, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&a) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to convert b to unstructured: %w", err) | ||
| } | ||
|  | ||
| unstructuredB, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&b) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to convert b to unstructured: %w", err) | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the unstructured conversion and error message.
runtime.DefaultUnstructuredConverter.ToUnstructured expects the concrete object, not a pointer to the interface wrapper. By passing &a / &b, we hand it values of type *interface{}, which the converter cannot handle, so every diff involving typed structs will error out. While you are there, the first error message also says “convert b”, making diagnostics misleading. Update both calls to pass a / b directly and correct the message. (reddit.com)
-	unstructuredA, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&a)
+	unstructuredA, err := runtime.DefaultUnstructuredConverter.ToUnstructured(a)
 	if err != nil {
-		return nil, fmt.Errorf("failed to convert b to unstructured: %w", err)
+		return nil, fmt.Errorf("failed to convert a to unstructured: %w", err)
 	}
-	unstructuredB, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&b)
+	unstructuredB, err := runtime.DefaultUnstructuredConverter.ToUnstructured(b)
 	if err != nil {
 		return nil, fmt.Errorf("failed to convert b to unstructured: %w", err)
 	}🤖 Prompt for AI Agents
In pkg/util/diff.go around lines 63 to 71, the calls pass &a and &b to
runtime.DefaultUnstructuredConverter.ToUnstructured and the first error message
incorrectly references "b"; change the calls to pass a and b (the concrete
values, not pointers to interface wrappers) and update the error messages to
"failed to convert a to unstructured" and "failed to convert b to unstructured"
respectively so conversion works and diagnostics are accurate.
| // Compare metadata | ||
| if diffMetadata, err := util.ObjectMetaEqual(metadata1, metadata2); err != nil { | ||
| return nil, fmt.Errorf("failed to compare Cluster API Infrastructure machine metadata: %w", err) | ||
| } else if diffMetadata.Changed() { | ||
| diff[".metadata"] = diffMetadata.String() | ||
| } | ||
|  | ||
| // TODO: Evaluate if we want to add status comparison if needed in the future (e.g. for scale from zero capacity). | ||
| // Compare spec | ||
| if diffSpec, err := util.NewDiffer().Diff(spec1, spec2); err != nil { | ||
| return nil, fmt.Errorf("failed to compare Cluster API Infrastructure machine spec: %w", err) | ||
| } else if diffSpec.Changed() { | ||
| diff[".spec"] = diffSpec.String() | ||
| } | ||
|  | ||
| return diff, nil | ||
| default: | ||
| return nil, fmt.Errorf("%w: %s", errPlatformNotSupported, platform) | ||
| // Compare status | ||
| if diffStatus, err := util.NewDiffer(util.WithIgnoreConditionsLastTransitionTime()).Diff(status1, status2); err != nil { | ||
| return nil, fmt.Errorf("failed to compare Cluster API Infrastructure machine status: %w", err) | ||
| } else if diffStatus.Changed() { | ||
| diff[".status"] = diffStatus.String() | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More generic as in abstracting each of the diff fields?
|  | ||
| out := "." + strings.Join(d.diff, ", .") | ||
| out = strings.ReplaceAll(out, ".slice[", "[") | ||
| out = strings.ReplaceAll(out, "map[", "[") | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be a nit, but could we distinguish maps from slices with {}? Might be too much work, since we'd have to find and replace the closing characters, but it could be helpful in interpreting output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pure cosmetics / output (I guess mostly for us when debugging).
I'm even okay with keeping the more verbose mode.
| @chrischdi: This pull request references OCPCLOUD-3172 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.21.0" version, but no target version was set. In response to this: 
 Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. | 
| @chrischdi: This pull request references OCPCLOUD-3172 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.21.0" version, but no target version was set. In response to this: 
 Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
pkg/util/diff.go (1)
126-134: Don't wrap the operands in&before converting to unstructured.
runtime.DefaultUnstructuredConverter.ToUnstructuredexpects the actual object. Passing&a/&bhands it*client.Object, so every diff on typed resources fails. While fixing that, please correct the first error message so it references “a”.- unstructuredA, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&a) - if err != nil { - return nil, fmt.Errorf("failed to convert b to unstructured: %w", err) + unstructuredA, err := runtime.DefaultUnstructuredConverter.ToUnstructured(a) + if err != nil { + return nil, fmt.Errorf("failed to convert a to unstructured: %w", err) } - unstructuredB, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&b) + unstructuredB, err := runtime.DefaultUnstructuredConverter.ToUnstructured(b)
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
📒 Files selected for processing (9)
- pkg/controllers/machinesetsync/machineset_sync_controller.go(11 hunks)
- pkg/controllers/machinesetsync/machineset_sync_controller_test.go(1 hunks)
- pkg/controllers/machinesetsync/machineset_sync_controller_unit_test.go(1 hunks)
- pkg/controllers/machinesync/machine_sync_controller.go(6 hunks)
- pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go(7 hunks)
- pkg/conversion/mapi2capi/util.go(1 hunks)
- pkg/util/diff.go(1 hunks)
- pkg/util/diff_test.go(1 hunks)
- pkg/util/sync.go(0 hunks)
💤 Files with no reviewable changes (1)
- pkg/util/sync.go
🚧 Files skipped from review as they are similar to previous changes (1)
- pkg/util/diff_test.go
🔇 Additional comments (13)
pkg/conversion/mapi2capi/util.go (1)
44-49: Deterministic tag ordering looks great.Sorting the AWS tags up front eliminates noisy diffs when we convert back and forth between map and slice representations.
pkg/controllers/machinesetsync/machineset_sync_controller_test.go (1)
1177-1188: Thanks for exercising the new diff surface.Checking
HasProviderSpecChanges()andHasChanges()here makes sure the platform-aware comparer behaves correctly.pkg/controllers/machinesetsync/machineset_sync_controller_unit_test.go (1)
31-351: Great coverage for DiffResult behaviour.These table-driven cases hit all the tricky status paths—including the v1beta1/v1beta2 condition handling and LastTransitionTime suppression—so we can trust the refactored comparer.
pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go (1)
63-82: DiffResult integration reads clean.Branching on
HasSpecChanges,HasMetadataChanges, andHasStatusChangeskeeps the reconciliation flow explicit while reusing the shared differ.pkg/controllers/machinesync/machine_sync_controller.go (5)
649-674: LGTM! Signature and logic updated correctly.The function signature has been appropriately updated to accept
util.DiffResult, and the conditional logic correctly uses the newHas*Changes()methods to determine if updates are needed.
697-700: LGTM! Error handling added correctly.The comparison function now properly returns and propagates errors, improving robustness of the diff operation.
1254-1279: LGTM! Refactored comparison functions.Both
compareCAPIMachinesandcompareMAPIMachineshave been properly refactored to:
- Use the new
util.NewDefaultDiffer()interface- Return structured
util.DiffResultinstead of maps- Propagate errors from diff computation
- Support platform-aware provider spec comparison (MAPI machines)
The platform-aware diffing with
WithProviderSpecand field ignore options incompareMAPIMachinesis a solid improvement for handling provider-specific differences.
1282-1331: LGTM! Status update functions refactored correctly.Both
ensureCAPIMachineStatusUpdatedandensureMAPIMachineStatusUpdatedhave been consistently updated to acceptutil.DiffResultand use theHasStatusChanges()method for determining if status updates are needed.Also applies to: 1363-1408
1411-1434: LGTM! Comprehensive change detection for MAPI machines.The function correctly checks for metadata, spec, and provider spec changes, which is appropriate given that MAPI machines have provider-specific specifications that need to be tracked separately.
pkg/controllers/machinesetsync/machineset_sync_controller.go (4)
787-814: LGTM! Proper error handling for comparison.The comparison function call correctly handles errors and propagates them with appropriate wrapping.
843-866: LGTM! All ensure functions refactored correctly.All four
ensure*Updatedfunctions have been consistently updated to:
- Accept
util.DiffResultinstead of map-based diffs- Use the appropriate
Has*Changes()methods- Apply correct conditional logic with proper negation for early returns
Also applies to: 869-917, 920-944, 947-996
1021-1024: LGTM! Platform parameter added correctly.The comparison function correctly receives the platform parameter to enable platform-aware provider spec diffing.
1317-1399: LGTM! Comparison functions properly refactored.All three comparison functions have been successfully refactored to:
- Return
(util.DiffResult, error)instead of map-based diffs- Use
util.NewDefaultDiffer()with appropriate configuration options- Handle platform-specific differences where needed (provider specs)
- Properly wrap and propagate errors
The platform-aware diffing in
compareCAPIInfraMachineTemplatesandcompareMAPIMachineSetswith support for provider specs and field filtering is a significant improvement.
| if capiInfraMachineTemplatesDiff.HasChanges() { | ||
| logger.Info("No changes detected for CAPI infra machine template") | ||
| return nil | ||
| } | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical logic error: Inverted condition.
The condition if capiInfraMachineTemplatesDiff.HasChanges() is inverted. When HasChanges() returns true, it means there ARE changes detected, but the code logs "No changes detected" and returns early, skipping the update. This will prevent legitimate updates from being applied.
Apply this fix:
-	if capiInfraMachineTemplatesDiff.HasChanges() {
+	if !capiInfraMachineTemplatesDiff.HasChanges() {
 		logger.Info("No changes detected for CAPI infra machine template")
 		return nil
 	}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if capiInfraMachineTemplatesDiff.HasChanges() { | |
| logger.Info("No changes detected for CAPI infra machine template") | |
| return nil | |
| } | |
| if !capiInfraMachineTemplatesDiff.HasChanges() { | |
| logger.Info("No changes detected for CAPI infra machine template") | |
| return nil | |
| } | 
🤖 Prompt for AI Agents
In pkg/controllers/machinesetsync/machineset_sync_controller.go around lines 742
to 745, the if-condition is inverted: when
capiInfraMachineTemplatesDiff.HasChanges() is true the code incorrectly logs "No
changes detected" and returns early; change the condition to if
!capiInfraMachineTemplatesDiff.HasChanges() to return early with the "No changes
detected" log, allowing the update path to execute when HasChanges() is true.
| @chrischdi: This pull request references OCPCLOUD-3172 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.21.0" version, but no target version was set. In response to this: 
 Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. | 
a9987c6    to
    dbbfcd3      
    Compare
  
    | @chrischdi: This pull request references OCPCLOUD-3172 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.21.0" version, but no target version was set. In response to this: 
 Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
pkg/controllers/machinesetsync/machineset_sync_controller.go (1)
742-745: Critical: Inverted condition prevents infrastructure template updates.The condition is inverted: when
HasChanges()returnstrue(changes exist), the code incorrectly logs "No changes detected" and returns early, preventing legitimate updates from being applied.Apply this fix:
- if capiInfraMachineTemplatesDiff.HasChanges() { + if !capiInfraMachineTemplatesDiff.HasChanges() { logger.Info("No changes detected for CAPI infra machine template") return nil }
🧹 Nitpick comments (2)
pkg/conversion/mapi2capi/util.go (1)
33-53: Export the sentinel error so callers can detect unsupported platforms.Because
ProviderSpecFromRawExtensionis exported, downstream callers will likely need to distinguish an unsupported platform from other parse failures. Keeping the sentinel unexported forces string matching; exporting it (e.g.,ErrUnsupportedPlatform) lets clients rely onerrors.Is. Please export the sentinel and update the reference accordingly.-var errUnsupportedPlatform = errors.New("unsupported platform") +var ErrUnsupportedPlatform = errors.New("unsupported platform") ... - return nil, fmt.Errorf("%w: %s", errUnsupportedPlatform, platform) + return nil, fmt.Errorf("%w: %s", ErrUnsupportedPlatform, platform)pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go (1)
248-301: LGTM: Platform-aware comparison successfully generalized.The refactored comparison function correctly handles platform-specific type assertions before delegating to the type-agnostic differ. The error handling is comprehensive, and the default case ensures unsupported platforms are caught.
Optional: Consider renaming
obj1andobj2totypedInfraMachine1andtypedInfraMachine2for consistency with the parameter names and to make the type assertion flow clearer.func compareCAPIInfraMachines(platform configv1.PlatformType, infraMachine1, infraMachine2 client.Object) (util.DiffResult, error) { - var obj1, obj2 client.Object + var typedInfraMachine1, typedInfraMachine2 client.Object switch platform { case configv1.AWSPlatformType: - typedInfraMachine1, ok := infraMachine1.(*awsv1.AWSMachine) + awsMachine1, ok := infraMachine1.(*awsv1.AWSMachine) if !ok { return nil, errAssertingCAPIAWSMachine } - typedinfraMachine2, ok := infraMachine2.(*awsv1.AWSMachine) + awsMachine2, ok := infraMachine2.(*awsv1.AWSMachine) if !ok { return nil, errAssertingCAPIAWSMachine } - obj1 = typedInfraMachine1 - obj2 = typedinfraMachine2 + typedInfraMachine1 = awsMachine1 + typedInfraMachine2 = awsMachine2 // ... similar for other platforms } - diff, err := util.NewDefaultDiffer().Diff(obj1, obj2) + diff, err := util.NewDefaultDiffer().Diff(typedInfraMachine1, typedInfraMachine2) if err != nil { return nil, fmt.Errorf("failed to compare Cluster API infrastructure machines: %w", err) } return diff, nil }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
📒 Files selected for processing (9)
- pkg/controllers/machinesetsync/machineset_sync_controller.go(11 hunks)
- pkg/controllers/machinesetsync/machineset_sync_controller_test.go(1 hunks)
- pkg/controllers/machinesetsync/machineset_sync_controller_unit_test.go(1 hunks)
- pkg/controllers/machinesync/machine_sync_controller.go(6 hunks)
- pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go(7 hunks)
- pkg/conversion/mapi2capi/util.go(1 hunks)
- pkg/util/diff.go(1 hunks)
- pkg/util/diff_test.go(1 hunks)
- pkg/util/sync.go(0 hunks)
💤 Files with no reviewable changes (1)
- pkg/util/sync.go
🚧 Files skipped from review as they are similar to previous changes (2)
- pkg/util/diff_test.go
- pkg/util/diff.go
🔇 Additional comments (20)
pkg/controllers/machinesetsync/machineset_sync_controller_unit_test.go (1)
30-351: Excellent test coverage for status diffing!The comprehensive test suite effectively validates the new DiffResult-based diff approach across multiple scenarios:
- Individual and combined field differences
- v1beta1 and v1beta2 conditions handling
- LastTransitionTime correctly ignored in comparisons
- nil vs non-nil status scenarios
The test structure is clear and the assertions properly validate both
HasChanges()and the diff string format.pkg/controllers/machinesetsync/machineset_sync_controller_test.go (1)
1175-1189: Test updates correctly reflect new diff API.The changes properly adapt to the platform-aware
compareMAPIMachineSetssignature and use the newDiffResultmethods (HasProviderSpecChanges(),HasChanges()) instead of map-based checks.pkg/controllers/machinesetsync/machineset_sync_controller.go (9)
787-790: Proper error handling for diff operation.The updated signature correctly propagates errors from
compareCAPIMachineSets, with appropriate error wrapping for context.
843-849: Clean integration with DiffResult API.The function signature and logic correctly use the new
DiffResultmethods (HasMetadataChanges(),HasSpecChanges(),HasProviderSpecChanges()) to determine when spec updates are needed.
869-875: Correct status update gating logic.The function properly uses
HasStatusChanges()and thespecUpdatedflag to determine when status updates are necessary, maintaining the synchronization semantics.
920-926: Consistent diff checking for MAPI spec updates.The implementation mirrors the CAPI version with appropriate use of
HasMetadataChanges(),HasSpecChanges(), andHasProviderSpecChanges()to detect when updates are needed.
947-953: Status update logic correctly adapted.The function properly integrates with the new
DiffResultAPI, usingHasStatusChanges()and thespecUpdatedflag to gate status updates appropriately.
1021-1024: Platform-aware diff comparison properly integrated.The call to
compareMAPIMachineSetscorrectly includes the platform parameter to enable platform-specific providerSpec handling, with appropriate error propagation.
1317-1370: Well-structured platform-aware infrastructure template comparison.The refactored function cleanly handles platform-specific types (AWS, OpenStack, PowerVS) and delegates to the new
util.NewDefaultDiffer()for consistent, generalized diffing with proper error handling.
1373-1380: Simplified and consistent CAPI MachineSet comparison.The refactored function correctly delegates to
util.NewDefaultDiffer()for consistent comparison logic, with proper error handling.
1383-1399: Comprehensive platform-aware MAPI MachineSet comparison.The refactored function properly configures the differ with:
- Platform-specific providerSpec handling via
WithProviderSpec- Appropriate status field exclusions (replicas, observedGeneration, etc.) that are managed separately by sync logic
- Clean error handling and propagation
pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go (3)
66-92: LGTM: Clean migration to DiffResult-based spec change detection.The refactored logic correctly uses
diff.HasSpecChanges()to detect when the infrastructure machine needs recreation due to immutable spec changes. The error handling and logging properly capture the diff details for debugging.
124-147: LGTM: Signature update aligns with the new differ approach.The function signature now accepts
util.DiffResult, and the logic correctly usesHasMetadataChanges()for early-exit optimization. This is a clean improvement over the previous map-based approach.
149-204: LGTM: Status update logic correctly uses DiffResult.The refactored signature and logic properly use
diff.HasStatusChanges()to determine when status updates are needed. The generation synchronization check (line 160) ensures consistency between MAPI and CAPI resources.pkg/controllers/machinesync/machine_sync_controller.go (6)
649-674: LGTM: Spec update logic cleanly migrated to DiffResult.The refactored function correctly uses
capiMachinesDiff.HasMetadataChanges()andcapiMachinesDiff.HasSpecChanges()to determine when updates are needed. The early-return optimization on line 653 is logically sound.
697-700: LGTM: Call site properly handles new comparison API.The error handling and return type match the updated
compareCAPIMachinessignature, with proper error wrapping.
751-754: LGTM: Platform parameter enables platform-aware diffing.The addition of the
r.Platformparameter is essential for the new platform-aware providerSpec comparison logic. This change aligns with the PR's refactoring objectives.
1254-1261: LGTM: Simplified and type-safe comparison for CAPI machines.The refactored implementation is clean and concise, delegating to the generalized differ. The error handling properly wraps errors for context. This is a significant improvement over the previous approach.
1264-1279: Excellent: Platform-aware MAPI comparison with providerSpec handling.This is a key improvement in the refactoring. The differ configuration correctly:
- Uses platform-specific providerSpec conversion via
WithProviderSpec(line 1266)- Ignores controller-managed status fields that should not affect sync decisions (lines 1268-1271)
- Enables deterministic, structured field-by-field comparison
The
mapi2capi.ProviderSpecFromRawExtensionconverter ensures platform-specific providerSpec schemas are handled correctly.
1411-1434: LGTM: MAPI spec update correctly includes providerSpec changes.The refactored logic properly checks for
HasProviderSpecChanges()in addition to metadata and spec changes (line 1415). This is essential for MAPI machines since their providerSpec field contains platform-specific configuration that requires special handling during comparison.
| @chrischdi: The following test failed, say  
 Full PR test history. Your PR dashboard. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here. | 
| @chrischdi: This pull request references OCPCLOUD-3172 which is a valid jira issue. Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "4.21.0" version, but no target version was set. In response to this: 
 Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository. | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (2)
pkg/util/diff.go (1)
130-138: Fix the unstructured conversion and error message.
runtime.DefaultUnstructuredConverter.ToUnstructuredexpects the concrete object, not a pointer to the interface wrapper. By passing&a/&b, you're handing it values of type*interface{}, which the converter cannot handle, so every diff involving typed structs will error out. Additionally, the first error message incorrectly says "convert b" when it should say "convert a".Apply this diff:
- unstructuredA, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&a) + unstructuredA, err := runtime.DefaultUnstructuredConverter.ToUnstructured(a) if err != nil { - return nil, fmt.Errorf("failed to convert b to unstructured: %w", err) + return nil, fmt.Errorf("failed to convert a to unstructured: %w", err) } - unstructuredB, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&b) + unstructuredB, err := runtime.DefaultUnstructuredConverter.ToUnstructured(b) if err != nil { return nil, fmt.Errorf("failed to convert b to unstructured: %w", err) }pkg/controllers/machinesetsync/machineset_sync_controller.go (1)
742-745: Critical logic error: Inverted condition.The condition
if capiInfraMachineTemplatesDiff.HasChanges()is inverted. WhenHasChanges()returnstrue, it means there ARE changes detected, but the code logs "No changes detected" and returns early, skipping the update. This will prevent legitimate updates from being applied.Apply this fix:
- if capiInfraMachineTemplatesDiff.HasChanges() { + if !capiInfraMachineTemplatesDiff.HasChanges() { logger.Info("No changes detected for CAPI infra machine template") return nil }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
📒 Files selected for processing (4)
- pkg/controllers/machinesetsync/machineset_sync_controller.go(11 hunks)
- pkg/conversion/mapi2capi/aws.go(2 hunks)
- pkg/conversion/mapi2capi/util.go(1 hunks)
- pkg/util/diff.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- pkg/conversion/mapi2capi/util.go
🔇 Additional comments (3)
pkg/conversion/mapi2capi/aws.go (1)
440-445: LGTM! Deterministic tag sorting.Sorting tags by name after unmarshalling ensures consistent ordering when converting between MAPI slice and CAPI map representations, reducing non-deterministic diffs. This pairs well with the new platform-aware differ framework.
pkg/controllers/machinesetsync/machineset_sync_controller.go (2)
787-790: LGTM! Improved error handling.Now properly capturing and propagating errors from
compareCAPIMachineSets, preventing silent comparison failures. The error wrapping provides clear context for debugging.
1383-1399: LGTM! Platform-aware MAPI diff implementation.The implementation correctly:
- Accepts platform for platform-specific providerSpec handling
- Uses
WithProviderSpecto extract and diff providerSpec separately (enablingHasProviderSpecChanges()checks)- Ignores status fields that are managed independently (observedGeneration, authoritativeAPI, synchronizedGeneration)
- Properly wraps and returns errors
This aligns well with the broader refactoring to generalized, platform-aware diffing.
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| ) | ||
|  | ||
| var errTimedOutWaitingForFeatureGates = errors.New("objects to diff cannot be nil") | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the mismatched error variable name.
The variable name errTimedOutWaitingForFeatureGates doesn't match its message "objects to diff cannot be nil". This appears to be a copy-paste error.
Apply this diff:
-var errTimedOutWaitingForFeatureGates = errors.New("objects to diff cannot be nil")
+var errObjectsToCompareCannotBeNil = errors.New("objects to diff cannot be nil")Then update line 126 accordingly:
 	if a == nil || b == nil {
-		return nil, errTimedOutWaitingForFeatureGates
+		return nil, errObjectsToCompareCannotBeNil
 	}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In pkg/util/diff.go around line 33, the error variable name
errTimedOutWaitingForFeatureGates is mismatched with its message "objects to
diff cannot be nil"; rename the variable to a matching identifier (for example
errObjectsToDiffCannotBeNil) and update any usages (including the reference at
line 126) to use the new variable name so the identifier and message are
consistent.
| } | ||
|  | ||
| if err := modifyFunc(unstructuredB); err != nil { | ||
| return nil, fmt.Errorf("failed to run modify function %son b: %w", funcName, err) | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing space in error message format string.
The format string is missing a space between %s and on b, which will produce error messages like "ProviderSpecon b" instead of "ProviderSpec on b".
Apply this diff:
-		if err := modifyFunc(unstructuredB); err != nil {
-			return nil, fmt.Errorf("failed to run modify function %son b: %w", funcName, err)
+		if err := modifyFunc(unstructuredB); err != nil {
+			return nil, fmt.Errorf("failed to run modify function %s on b: %w", funcName, err)
 		}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return nil, fmt.Errorf("failed to run modify function %son b: %w", funcName, err) | |
| if err := modifyFunc(unstructuredB); err != nil { | |
| return nil, fmt.Errorf("failed to run modify function %s on b: %w", funcName, err) | |
| } | 
🤖 Prompt for AI Agents
In pkg/util/diff.go around line 151, the error format string is missing a space
between the %s and "on b"; update the format string from "%son b: %w" to "%s on
b: %w" so the error message prints "ProviderSpec on b" instead of
"ProviderSpecon b".
Summary by CodeRabbit