Skip to content

Conversation

@honza
Copy link
Member

@honza honza commented Oct 2, 2025

Assisted-by: claude-4.5-sonnet

Summary by CodeRabbit

  • New Features
    • Full Bare Metal (Metal3) platform support for migration and sync flows, including bidirectional Machine/MachineSet conversions and controller enablement.
  • Improvements
    • More accurate diff/compare behavior (including infra status) and expanded conversion diagnostics; new debug/verification flags for build/localize and Helm debug support.
  • Chores
    • Updated dependencies and vendor additions (diff library, license/contributors, kustomize updates) and assorted tooling/version bumps.

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Oct 2, 2025
@openshift-ci-robot
Copy link

openshift-ci-robot commented Oct 2, 2025

@honza: This pull request references METAL-1583 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:

Assisted-by: claude-4.5-sonnet

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.

@coderabbitai
Copy link

coderabbitai bot commented Oct 2, 2025

Walkthrough

Adds Metal3 (Bare Metal) support across migration: registers Metal3 scheme, extends controllers and infra-init/compare logic for BareMetal, and adds bidirectional conversion implementations between MAPI BareMetal and CAPI Metal3 Machine/MachineSet types, plus related dependency and vendor updates.

Changes

Cohort / File(s) Summary of changes
CLI wiring & scheme registration
cmd/machine-api-migration/main.go
Register Metal3 v1beta1 scheme and enable BareMetalPlatformType in provider switch at startup.
Controller helpers
pkg/controllers/common.go
Add BareMetal branches returning Metal3Machine/Metal3Cluster and Metal3MachineTemplate/Metal3Cluster from infra-init helpers.
MachineSet sync controller
pkg/controllers/machinesetsync/machineset_sync_controller.go
Add BareMetal handling for CAPI↔MAPI MachineSet conversions: Metal3MachineTemplate/Metal3Cluster assertions, list init, compare/diff logic, and new assertion error symbol.
Machine sync controller
pkg/controllers/machinesync/machine_sync_controller.go
Add BareMetal handling for CAPI↔MAPI Machine conversions: Metal3Machine/Metal3Cluster assertions and Metal3-specific conversion routing; new assertion error symbol.
CAPI → MAPI conversions (Metal3)
pkg/conversion/capi2mapi/metal3.go
New conversion layer translating CAPI Machine/MachineSet + Metal3 infra into MAPI BareMetal Machine/MachineSet with constructors, ToMachine/ToMachineSet, provider-spec mapping, helpers, and aggregated warnings/errors.
MAPI → CAPI conversions (Metal3)
pkg/conversion/mapi2capi/metal3.go
New conversion layer translating MAPI BareMetal Machine/MachineSet into CAPI Machine + Metal3 infra (Metal3Machine/Metal3MachineTemplate), with constructors, ToMachineAndInfrastructureMachine/ToMachineSetAndMachineTemplate, providerSpec decoding, helpers, and aggregated warnings/errors.
CAPI infra compare updates
pkg/controllers/machine_sync_mapi2capi_infrastructure.go
BareMetal (Metal3) infra comparison now asserts Metal3Machine types, compares spec/metadata and also status for diffs; lint adjustments.
Dependency changes
go.mod, e2e/go.mod, vendor/modules.txt
Add cluster-api-provider-baremetal dependency; add metal3 replacement mapping; bump cluster-api-provider-baremetal and kustomize deps; add sergi/go-diff diffmatchpatch transitive entry.
Vendor: diffmatchpatch
vendor/github.com/sergi/go-diff/...
Add diff-match-patch implementation and supporting files (diff, match, patch, utilities, license, authors, contributors).
Vendor: kustomize / kyaml
vendor/sigs.k8s.io/kustomize/...
Multiple kustomize updates: replace custom slice helpers with stdlib slices, add helm-debug flag, remove Starlark/Star flags, add localize verification, add kyaml/copyutil (CopyDir/Diff/SyncFile) utilities, and assorted minor CLI/validation tweaks.
Misc vendor files
vendor/...
Add AUTHORS/CONTRIBUTORS/LICENSE for go-diff and small vendor validation/usage changes (e.g., grep arg validation).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Controller
  participant MAPI as MAPI BareMetal (Machine/Set)
  participant Infra as OpenShift Infrastructure
  participant MAPI2CAPI as mapi2capi Metal3 Converter
  participant CAPI as CAPI Machine/Set
  participant Metal3 as Metal3 infra (Machine/Template/Cluster)

  Controller->>MAPI: Read BareMetal Machine/Set
  Controller->>Infra: Read Infrastructure
  Controller->>MAPI2CAPI: FromMetal3Machine[Set]AndInfra(...)
  MAPI2CAPI->>MAPI2CAPI: Decode providerSpec, map fields, collect warnings/errors
  MAPI2CAPI->>Metal3: Build Metal3Machine / Metal3MachineTemplate
  MAPI2CAPI->>CAPI: Build CAPI Machine/MachineSet referencing Metal3 infra
  MAPI2CAPI-->>Controller: Return CAPI + Metal3 objects + warnings
Loading
sequenceDiagram
  autonumber
  actor Controller
  participant CAPI as CAPI Machine/Set
  participant Metal3 as Metal3 infra (Machine/Template/Cluster)
  participant CAPI2MAPI as capi2mapi Metal3 Converter
  participant MAPI as MAPI BareMetal (Machine/Set)

  Controller->>CAPI: Read CAPI Machine/Set
  Controller->>Metal3: Read Metal3 infra refs
  Controller->>CAPI2MAPI: FromMachine[Set]AndMetal3...(...)
  CAPI2MAPI->>CAPI2MAPI: Translate Metal3 fields to BareMetal providerSpec
  CAPI2MAPI->>MAPI: Construct MAPI BareMetal Machine/Set (RawExtension)
  CAPI2MAPI-->>Controller: Return MAPI objects + warnings
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas to inspect closely:

  • pkg/conversion/mapi2capi/metal3.go and pkg/conversion/capi2mapi/metal3.go for correctness of providerSpec mappings and error/warning aggregation.
  • compare/ diff code paths in machinesync/machineset_sync_controller.go and machine_sync_mapi2capi_infrastructure.go to ensure proper Metal3 type assertions and .status handling.
  • go.mod / vendor updates for consistency and module replacement correctness.

Poem

I thump my paws—new rails to lay,
Metal3 meets fields of hay!
Machines hop from spec to spec,
Bare metal paths now interconnect.
Controllers hum—migration day! 🐇⚙️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.52% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "METAL-1583: baremetal: implement MAPI/CAPI migration" is fully aligned with the changeset. The changes comprehensively add Bare Metal (Metal3) platform support across migration and sync flows, including Metal3 schema registration, conversion implementations for bidirectional MAPI/CAPI translation, extended controller logic for Bare Metal platform handling, and dependency updates. The title is concise, specific, and clearly conveys the primary objective without vague language—a teammate scanning the history would immediately understand that this PR implements Metal3 migration capabilities.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

@openshift-ci openshift-ci bot requested review from chrischdi and damdo October 2, 2025 19:04
@openshift-ci
Copy link
Contributor

openshift-ci bot commented Oct 2, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign theobarberbany for approval. For more information see the Code Review Process.

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 /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci-robot
Copy link

openshift-ci-robot commented Oct 2, 2025

@honza: This pull request references METAL-1583 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:

Assisted-by: claude-4.5-sonnet

Summary by CodeRabbit

  • New Features

  • Added Bare Metal (Metal3) platform support across migration and sync flows.

  • Enabled controllers to run on Bare Metal clusters and process Metal3 resources.

  • Implemented bidirectional conversion between CAPI and MAPI for Bare Metal Machines and MachineSets.

  • Enhanced comparison and diffing to handle Metal3 templates and machines.

  • Improved error handling and warnings during Bare Metal conversions.

  • Chores

  • Integrated required schemas to register Metal3 resources.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
cmd/machine-api-migration/main.go (1)

199-208: Update the outdated comment.

Line 199 states "Currently we only plan to support AWS" but the code now supports AWS, Bare Metal, and OpenStack. The Bare Metal addition is correct.

Apply this diff to update the comment:

-	// Currently we only plan to support AWS, so all others are a noop until they're implemented.
+	// Currently we support AWS, Bare Metal, and OpenStack. Other platforms are a noop until implemented.
📜 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5d63584 and 70de0dd.

📒 Files selected for processing (6)
  • cmd/machine-api-migration/main.go (3 hunks)
  • pkg/controllers/common.go (3 hunks)
  • pkg/controllers/machinesetsync/machineset_sync_controller.go (6 hunks)
  • pkg/controllers/machinesync/machine_sync_controller.go (5 hunks)
  • pkg/conversion/capi2mapi/metal3.go (1 hunks)
  • pkg/conversion/mapi2capi/metal3.go (1 hunks)
🔇 Additional comments (19)
pkg/conversion/capi2mapi/metal3.go (7)

33-36: LGTM!

The error variable declarations are consistent with the existing codebase patterns and provide clear error messages for nil validation.


38-50: LGTM!

The type definitions follow a clean composition pattern, with the MachineSet struct properly embedding the Machine struct to enable code reuse during conversion.


52-76: LGTM!

The constructor functions follow the established pattern and correctly extract Machine template data from MachineSet specs for reuse in the conversion logic.


119-154: LGTM!

The ToMachine method properly validates inputs, handles errors, aggregates warnings, and follows the established conversion pattern.


156-194: LGTM!

The ToMachineSet method properly validates inputs and reuses ToMachine for spec validation. The nolint:dupl directive is expected as this pattern is replicated across platform implementations.


196-239: LGTM!

The conversion helper functions properly map fields between Metal3 and BareMetal types, handle nil cases appropriately, and include helpful comments about ignored fields.


241-267: LGTM!

The validation function properly identifies and reports unsupported Metal3Machine fields with clear error messages and appropriate field paths.

pkg/controllers/common.go (2)

46-47: LGTM!

The Bare Metal case correctly returns Metal3Machine and Metal3Cluster types, following the established pattern for other platforms.


65-66: LGTM!

The Bare Metal case correctly returns Metal3MachineTemplate and Metal3Cluster types, maintaining consistency with the Machine variant above.

cmd/machine-api-migration/main.go (1)

72-72: LGTM!

The Metal3 scheme registration is correctly added and properly positioned between AWS and OpenStack, ensuring the controller runtime can work with Metal3 types.

pkg/controllers/machinesync/machine_sync_controller.go (4)

98-99: LGTM!

The error variable definition follows the established pattern and is logically placed among similar error variables.


566-567: LGTM!

The Bare Metal conversion case correctly calls the MAPI to CAPI converter, following the established pattern.


591-602: LGTM!

The Bare Metal case properly type-asserts Metal3Machine and Metal3Cluster before calling the CAPI to MAPI converter, with appropriate error handling.


1387-1408: LGTM!

The Bare Metal comparison case correctly type-asserts Metal3Machine instances and performs spec and metadata diffs, following the established pattern.

pkg/controllers/machinesetsync/machineset_sync_controller.go (5)

81-82: LGTM!

The error variable definition follows the established pattern and is logically placed among similar MachineTemplate assertion errors.


674-687: LGTM!

The Bare Metal case properly type-asserts Metal3MachineTemplate and Metal3Cluster before calling the CAPI to MAPI MachineSet converter, following the established pattern.


726-727: LGTM!

The Bare Metal conversion case correctly calls the MAPI to CAPI MachineSet converter, maintaining consistency with other platforms.


1330-1331: LGTM!

The Bare Metal case correctly returns Metal3MachineTemplateList and Metal3ClusterList, following the established pattern.


1370-1393: LGTM!

The Bare Metal comparison case correctly type-asserts Metal3MachineTemplate instances and performs spec and metadata diffs, following the established pattern. The TODO comment about status comparison is consistent with other platforms.

Comment on lines 90 to 95
TypeMeta: metav1.TypeMeta{
Kind: "BareMetalMachineProviderSpec",
APIVersion: "baremetal.cluster.gke.io/v1alpha1",
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Correct the APIVersion for BareMetalMachineProviderSpec.

Line 92 specifies APIVersion: "baremetal.cluster.gke.io/v1alpha1" which appears to be incorrect. This is an OpenShift/Red Hat bare metal provider, not a GKE provider.

Run the following script to find the correct APIVersion used in the codebase:


🏁 Script executed:

#!/bin/bash
# Description: Find the correct APIVersion for BareMetalMachineProviderSpec in the codebase

# Search for BareMetalMachineProviderSpec type definition and its usage
rg -nP --type=go -A5 'type\s+BareMetalMachineProviderSpec\s+struct'

# Search for existing usages of BareMetalMachineProviderSpec with APIVersion
rg -nP --type=go 'APIVersion.*baremetal'

Length of output: 1064


Update APIVersion for BareMetalMachineProviderSpec
In pkg/conversion/capi2mapi/metal3.go:92, change
APIVersion: "baremetal.cluster.gke.io/v1alpha1"
to
APIVersion: "baremetal.openshift.io/v1alpha1"
matching the provider’s SchemeGroupVersion.

🤖 Prompt for AI Agents
In pkg/conversion/capi2mapi/metal3.go around lines 90 to 93, the TypeMeta
APIVersion is set to "baremetal.cluster.gke.io/v1alpha1" but must match the
provider's SchemeGroupVersion; update the APIVersion value to
"baremetal.openshift.io/v1alpha1" so the TypeMeta reads APIVersion:
"baremetal.openshift.io/v1alpha1".

Comment on lines +122 to +127
if m.infrastructure == nil || m.infrastructure.Status.InfrastructureName == "" {
errs = append(errs, field.Invalid(field.NewPath("infrastructure", "status", "infrastructureName"), m.infrastructure.Status.InfrastructureName, "infrastructure cannot be nil and infrastructure.Status.InfrastructureName cannot be empty"))
} else {
capiMachine.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
capiMachine.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Avoid nil dereference when infra is absent.

Inside the if m.infrastructure == nil || ... branch we still evaluate m.infrastructure.Status.InfrastructureName, which panics as soon as m.infrastructure is nil. Capture the value in a local only when the struct is non-nil (or split the branch) before calling field.Invalid.

Comment on lines +125 to +127
capiMachine.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
capiMachine.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Initialize labels before writing to them.

capiMachine.Labels can be nil (the zero value from the conversion helper), so assigning capiMachine.Labels[...] = ... risks a panic. Ensure the map is initialized (e.g. if capiMachine.Labels == nil { capiMachine.Labels = map[string]string{} }) before writing.

🤖 Prompt for AI Agents
In pkg/conversion/mapi2capi/metal3.go around lines 125 to 127,
capiMachine.Labels may be nil and writing to it risks a panic; ensure the map is
initialized before assigning to it by checking if capiMachine.Labels == nil and,
if so, setting it to an empty map[string]string, then perform the label
assignment as shown.

@openshift-ci-robot
Copy link

openshift-ci-robot commented Oct 2, 2025

@honza: This pull request references METAL-1583 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:

Assisted-by: claude-4.5-sonnet

Summary by CodeRabbit

  • New Features

  • Added Bare Metal (Metal3) platform support across migration and sync flows.

  • Controllers can run on Bare Metal clusters and process Metal3 resources.

  • Implemented bidirectional conversion between CAPI and MAPI for Bare Metal Machines and MachineSets, including provider-spec mappings, template generation, and diffs.

  • Enhanced comparison and diffing to handle Metal3 templates and machines, with improved warnings.

  • Chores

  • Registered Metal3 schemas so Metal3 resources are recognized.

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.

@openshift-ci-robot
Copy link

openshift-ci-robot commented Oct 2, 2025

@honza: This pull request references METAL-1583 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:

Assisted-by: claude-4.5-sonnet

Summary by CodeRabbit

  • New Features
  • Full Bare Metal (Metal3) platform support for migration and sync flows with bidirectional Machine/MachineSet conversions and controller runtime support.
  • Improvements
  • Improved diff/patch utilities and comparison flows for more accurate change detection and diagnostics.
  • New debug/verification flags for build/localize flows to aid troubleshooting.
  • Chores
  • Updated dependencies and vendor additions (including diff library and license/contributor files).

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vendor/sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/api/docs.go (1)

147-151: Doc snippet: missing closing quote in CMD example.

The Dockerfile snippet shows: CMD ["nginx-template.sh] — missing the closing quote. Should be CMD ["nginx-template.sh"].

Consider re-generating docs from upstream or fixing the generated input to avoid editing vendor manually.

Suggested correction:

-	CMD ["nginx-template.sh]
+	CMD ["nginx-template.sh"]
🧹 Nitpick comments (4)
vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (2)

124-127: Library function prints to stdout; consider making this opt-in.

Unconditional fmt.Println in Diff can be noisy in libraries. Prefer returning diffs to caller or gating via a flag/logger.


190-197: deleteFile swallows non-NotExist errors.

Return non-ENOENT errors from Stat to avoid hiding permission/IO problems.

- if _, err := os.Stat(path); err != nil {
-     // return nil if file doesn't exist
-     return nil
- }
+ if _, err := os.Stat(path); err != nil {
+     if os.IsNotExist(err) {
+         return nil
+     }
+     return err
+ }
  return os.Remove(path)
go.mod (1)

308-308: Add rationale comment for replace directive
This replace forces any import of cluster-api-provider-metal3/api (including the v0.0.0-… pseudo-version pulled in by the baremetal provider) to v1.10.1 for consistent dependency resolution. Annotate this in go.mod above the directive.

vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go (1)

1091-1120: Potential byte vs rune mismatch in DiffXIndex

This counts len(aDiff.Text) bytes. Elsewhere, lengths often use utf8.RuneCountInString. For non‑ASCII text, byte counting can mis-map locations.

Consider switching to rune counts consistently, or clearly document that loc is byte‑based. Example change:

-    if aDiff.Type != DiffInsert { chars1 += len(aDiff.Text) }
-    if aDiff.Type != DiffDelete { chars2 += len(aDiff.Text) }
+    if aDiff.Type != DiffInsert { chars1 += utf8.RuneCountInString(aDiff.Text) }
+    if aDiff.Type != DiffDelete { chars2 += utf8.RuneCountInString(aDiff.Text) }

If API consumers pass byte offsets, keep as bytes but add a comment to avoid confusion.

📜 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

📥 Commits

Reviewing files that changed from the base of the PR and between 629f160 and adcf11c.

⛔ Files ignored due to path filters (4)
  • e2e/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
  • go.work.sum is excluded by !**/*.sum
  • hack/tools/go.sum is excluded by !**/*.sum
📒 Files selected for processing (39)
  • e2e/go.mod (1 hunks)
  • go.mod (2 hunks)
  • hack/tools/go.mod (2 hunks)
  • vendor/github.com/sergi/go-diff/AUTHORS (1 hunks)
  • vendor/github.com/sergi/go-diff/CONTRIBUTORS (1 hunks)
  • vendor/github.com/sergi/go-diff/LICENSE (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/match.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go (1 hunks)
  • vendor/modules.txt (7 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/grep.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/api/docs.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/build.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagenablehelm.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagsforfunctions.go (0 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/create/create.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbase.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbuildmetadata.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addcomponent.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addgenerator.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addmetadata.go (5 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addresource.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addtransformer.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/fix/convert.go (3 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removebuildmetadata.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeconfigmap.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removemetadata.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeresource.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removesecret.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removetransformer.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile/kustomizationfile.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/util/validate.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/localize/localize.go (6 hunks)
  • vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (1 hunks)
💤 Files with no reviewable changes (1)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagsforfunctions.go
✅ Files skipped from review due to trivial changes (5)
  • vendor/github.com/sergi/go-diff/CONTRIBUTORS
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removemetadata.go
  • vendor/github.com/sergi/go-diff/AUTHORS
  • vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go
  • vendor/github.com/sergi/go-diff/LICENSE
🔇 Additional comments (19)
vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (2)

23-23: LGTM!

Doc comment correctly aligned with the function name.


23-23: Inconsistency between AI summary and actual code.

The AI summary states that Star/Starlark-related fields (StarPath, StarURL, StarName) and their usage were removed. However, the provided code still shows:

  • Lines 86-88: StarPath, StarURL, StarName fields in the RunFnRunner struct.
  • Line 108: Condition still checks r.StarPath == "" && r.StarURL == "".

Yet Line 251 no longer validates star-path/url when args are provided after --, which is inconsistent with the logic at Line 108.

This suggests either:

  1. The AI summary is incorrect about the removal.
  2. The code changes are incomplete (fields declared but not removed, validation partially updated).

Since this is a vendored file, verify whether the upstream kustomize dependency intended to remove star support fully or partially.

Also applies to: 251-251

vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/grep.go (1)

57-59: Verify vendored dependency updates are intentional

This PR modifies many files under vendor/ (go-diff, kustomize v5 commands, modules.txt). Confirm that bumping these dependencies is deliberate and pertains to the MAPI/CAPI migration. If not, revert or extract the vendor changes into a separate, focused PR.

hack/tools/go.mod (1)

11-11: Version bumps look good; ensure toolchain/builders align.

kustomize v5.6.0 and cmd/config v0.19.0 match kyaml/api 0.19.x. Please ensure CI/builders use Go >=1.21 (vendor uses slices) and that go.sum/vendor are consistent post-bump.

Also applies to: 216-216

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbuildmetadata.go (1)

8-8: Use of slices.Contains is correct and simplifies logic.

Looks good; ensure build uses Go >=1.21, which is already implied by the toolchain in hack/tools.

Also applies to: 64-68

vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go (1)

1-17: Generated String() implementation is correct.

Enum offset and bounds checking follow standard stringer output. No issues.

vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go (1)

1-107: Utility functions align with canonical go-diff; LGTM.

vendor/github.com/sergi/go-diff/diffmatchpatch/match.go (1)

1-161: Bitap matching implementation matches upstream behavior.

Edge cases and scoring look correct.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeresource.go (1)

77-77: LGTM: Standard library usage improves maintainability.

The refactor from kustfile.StringInSlice to slices.Contains is correct and aligns with using the standard library for membership checks. Note that the argument order has been correctly swapped to match slices.Contains(haystack, needle).

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/create/create.go (1)

117-117: LGTM: Consistent with standard library usage.

The change to slices.Contains is correct and consistent with the broader refactor across kustomize commands. The logic remains functionally equivalent.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/build.go (1)

33-33: LGTM: Helm debug flag correctly integrated.

The new helmDebug flag is properly defined and propagated to HelmConfig.Debug. This enables users to enable debug output from the Helm chart inflator.

Also applies to: 167-167

e2e/go.mod (1)

14-14: LGTM: Dependency version update for Bare Metal support.

The cluster-api-provider-baremetal dependency has been updated to a newer version, aligning with the Bare Metal integration work in this PR.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagenablehelm.go (1)

34-38: LGTM: Helm debug flag registration is correct.

The --helm-debug flag is properly registered with a clear description and appropriate default value (false). This complements the flag usage in build.go.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removesecret.go (1)

93-93: LGTM: Standard library usage for membership check.

The refactor to slices.Contains is correct and consistent with the broader pattern across kustomize commands. The logic remains functionally equivalent.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addresource.go (1)

77-77: LGTM: Consistent refactor to standard library.

The change to slices.Contains is correct and aligns with the broader refactor across kustomize add/remove commands. The logic is functionally equivalent.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbase.go (1)

1-77: Verify intentional edits to vendored dependencies

Vendor directory changes should be deliberate and maintained via Go modules. Please confirm:

  1. These patches have been upstreamed to the kustomize repo or otherwise reconciled.
  2. go mod vendor was run after updating dependencies.
  3. All modified vendor files in this PR:
base=$(git merge-base HEAD main)
git diff --name-only $base HEAD | grep '^vendor/'
vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go (3)

86-99: Vendored diffmatchpatch package is complete All referenced symbols (DiffMatchPatch, unescaper, max, runesEqual, runesIndex, runesIndexOf, indexOf, intArrayToString) are defined in vendor/github.com/sergi/go-diff/diffmatchpatch.


1-7: Vendored dependency includes LICENSE
The file vendor/github.com/sergi/go-diff/LICENSE is present as referenced in the headers.


405-423: Comma splitting safely preserves multi-digit indices
intArrayToString emits comma-separated decimal strings (with no embedded commas), so strings.Split(aDiff.Text, ",") cannot fragment multi-digit numbers.

Comment on lines +755 to +757
blankLine1 := lineBreak1 && blanklineEndRegex.MatchString(one)
blankLine2 := lineBreak2 && blanklineEndRegex.MatchString(two)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix boundary regex: use blanklineStartRegex for the trailing side

blankLine2 should match the start-of-blank-line on the right segment. Using blanklineEndRegex on both sides skews semantic scoring and can worsen cleanup results.

-	blankLine2 := lineBreak2 && blanklineEndRegex.MatchString(two)
+	blankLine2 := lineBreak2 && blanklineStartRegex.MatchString(two)
📝 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.

Suggested change
blankLine1 := lineBreak1 && blanklineEndRegex.MatchString(one)
blankLine2 := lineBreak2 && blanklineEndRegex.MatchString(two)
blankLine1 := lineBreak1 && blanklineEndRegex.MatchString(one)
blankLine2 := lineBreak2 && blanklineStartRegex.MatchString(two)
🤖 Prompt for AI Agents
In vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go around lines 755 to
757, blankLine2 is currently computed using blanklineEndRegex but should test
the start-of-blank-line on the right segment; change the computation so
blankLine2 uses blanklineStartRegex (e.g. set blankLine2 := lineBreak2 &&
blanklineStartRegex.MatchString(two)) so the boundary regexes reflect left/end
vs right/start correctly.

Comment on lines +35 to 37
buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer)
cmd := &cobra.Command{
Use: "localize [target [destination]]",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix verification buffer handling

buildBuffer is copied when passed into runBuildCmd. We reset and read only the copy, while buildCmd keeps writing to the original that never gets cleared. As a result both builds appear empty, so verification always “succeeds” even when localization changes output. Pass the pointer through (and update the call sites) so we reset/read the actual writer.

@@
-	var buildBuffer bytes.Buffer
-	buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer)
+	var buildBuffer bytes.Buffer
+	buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer)
@@
-				originalBuild, err := runBuildCmd(buildBuffer, buildCmd, args.target)
+				originalBuild, err := runBuildCmd(&buildBuffer, buildCmd, args.target)
@@
-				localizedBuild, err := runBuildCmd(buildBuffer, buildCmd, buildDst)
+				localizedBuild, err := runBuildCmd(&buildBuffer, buildCmd, buildDst)
@@
-func runBuildCmd(buffer bytes.Buffer, cmd *cobra.Command, folder string) (buildOutput string, err error) {
-	buffer.Reset()
+func runBuildCmd(buffer *bytes.Buffer, cmd *cobra.Command, folder string) (buildOutput string, err error) {
+	buffer.Reset()
@@
-	return buffer.String(), nil
+	return buffer.String(), nil

Also applies to: 74-94, 133-144

Comment on lines +34 to +52
// path is an absolute path, rather than a path relative to src.
// e.g. if src is /path/to/package, then path might be /path/to/package/and/sub/dir
// we need the path relative to src `and/sub/dir` when we are copying the files to dest.
copyTo := strings.TrimPrefix(path, src)

// make directories that don't exist
if info.IsDir() {
return errors.Wrap(fSys.MkdirAll(filepath.Join(dst, copyTo)))
}

// copy file by reading and writing it
b, err := fSys.ReadFile(filepath.Join(src, copyTo))
if err != nil {
return errors.Wrap(err)
}
err = fSys.WriteFile(filepath.Join(dst, copyTo), b)
if err != nil {
return errors.Wrap(err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: CopyDir may write outside dst due to leading separator in copyTo.

Using strings.TrimPrefix(path, src) can leave a leading path separator, causing filepath.Join(dst, copyTo) to ignore dst. Use filepath.Rel to ensure a proper relative path.

Apply this fix:

-        // don't copy the .git dir
-        if path != src {
-            rel := strings.TrimPrefix(path, src)
-            if IsDotGitFolder(rel) {
-                return nil
-            }
-        }
-
-        // path is an absolute path, rather than a path relative to src.
-        // e.g. if src is /path/to/package, then path might be /path/to/package/and/sub/dir
-        // we need the path relative to src `and/sub/dir` when we are copying the files to dest.
-        copyTo := strings.TrimPrefix(path, src)
+        // Compute path relative to src to avoid leading separators.
+        relPath, relErr := filepath.Rel(src, path)
+        if relErr != nil {
+            return errors.Wrap(relErr)
+        }
+        // don't copy the .git dir
+        if path != src && IsDotGitFolder(relPath) {
+            return nil
+        }
+        copyTo := relPath
-        if info.IsDir() {
-            return errors.Wrap(fSys.MkdirAll(filepath.Join(dst, copyTo)))
-        }
+        if info.IsDir() {
+            return errors.Wrap(fSys.MkdirAll(filepath.Join(dst, copyTo)))
+        }
-        b, err := fSys.ReadFile(filepath.Join(src, copyTo))
+        b, err := fSys.ReadFile(filepath.Join(src, copyTo))
-        err = fSys.WriteFile(filepath.Join(dst, copyTo), b)
+        err = fSys.WriteFile(filepath.Join(dst, copyTo), b)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go around lines 34-52,
replace the strings.TrimPrefix logic with a safe relative-path computation: call
filepath.Rel(src, path) and handle its error, then reject any returned rel that
is ".." or starts with ".."+os.PathSeparator to prevent escaping dst, and use
filepath.Clean(rel) when joining; use filepath.Join(dst, rel) for both MkdirAll
and ReadFile/WriteFile paths and wrap errors as before. Ensure you import os if
needed and return an error when the relative path is invalid.

@openshift-ci-robot
Copy link

openshift-ci-robot commented Oct 3, 2025

@honza: This pull request references METAL-1583 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:

Assisted-by: claude-4.5-sonnet

Summary by CodeRabbit

  • New Features
  • Full Bare Metal (Metal3) platform support for migration and sync flows, including controller runtime enablement and bidirectional Machine/MachineSet conversions.
  • Improvements
  • Enhanced diff/patch and comparison logic for more accurate change detection and diagnostics.
  • New debug and verification flags for build/localize flows to aid troubleshooting.
  • Chores
  • Updated dependencies and vendor additions (including diff library, license and contributor files).

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addmetadata.go (1)

1-216: Revert direct modifications to vendored kustomize code; update dependency instead
Files under vendor/sigs.k8s.io/kustomize must only change via an upstream version bump, not by direct edits. Please remove these changes and bump or upstream them appropriately.

vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (2)

86-88: Remove orphaned Star-related fields from RunFnRunner struct.

The AI summary indicates Star-based function support was removed, including flags and the fnAnnotationForStar function. However, the StarPath, StarURL, and StarName fields remain in the struct definition.

These fields appear to be orphaned—they're referenced in validation logic (line 108) but are no longer settable via flags, making them dead code that cannot be reached.

Consider removing these fields in a follow-up to complete the Star removal:

 type RunFnRunner struct {
 	IncludeSubpackages bool
 	Command            *cobra.Command
 	DryRun             bool
 	GlobalScope        bool
 	FnPaths            []string
 	Image              string
-	StarPath           string
-	StarURL            string
-	StarName           string
 	EnableExec         bool
 	ExecPath           string
 	RunFns             runfn.RunFns
 	ResultsDir         string
 	Network            bool
 	Mounts             []string
 	LogSteps           bool
 	Env                []string
 	AsCurrentUser      bool
 }

108-124: Simplify condition to remove unreachable Star checks.

Line 108 checks r.StarPath and r.StarURL, but:

  1. No command-line flags populate these fields (removed per AI summary)
  2. The switch statement (lines 119-124) only handles Image and ExecPath cases
  3. This creates dead code that can never execute

Simplify the condition to match the actual supported cases:

 func (r *RunFnRunner) getContainerFunctions(dataItems []string) (
 	[]*yaml.RNode, error) {
-	if r.Image == "" && r.StarPath == "" && r.ExecPath == "" && r.StarURL == "" {
+	if r.Image == "" && r.ExecPath == "" {
 		return nil, nil
 	}
pkg/controllers/machinesetsync/machineset_sync_controller.go (1)

369-396: Add Metal3 to filterOutdatedInfraMachineTemplates (otherwise cleanup fails on Bare Metal)

Without handling Metal3MachineTemplateList, outdated InfraMachineTemplates won’t be deleted for BareMetal clusters. Add a Metal3 branch.

 func filterOutdatedInfraMachineTemplates(infraMachineTemplateList client.ObjectList, newInfraMachineTemplateName string) ([]client.Object, error) {
   outdatedTemplates := []client.Object{}
 
   switch list := infraMachineTemplateList.(type) {
   case *awsv1.AWSMachineTemplateList:
     for _, template := range list.Items {
       if template.GetName() != newInfraMachineTemplateName {
         outdatedTemplates = append(outdatedTemplates, &template)
       }
     }
+  case *metal3v1.Metal3MachineTemplateList:
+    for _, template := range list.Items {
+      if template.GetName() != newInfraMachineTemplateName {
+        outdatedTemplates = append(outdatedTemplates, &template)
+      }
+    }
   case *ibmpowervsv1.IBMPowerVSMachineTemplateList:
     for _, template := range list.Items {
       if template.GetName() != newInfraMachineTemplateName {
         outdatedTemplates = append(outdatedTemplates, &template)
       }
     }
   case *openstackv1.OpenStackMachineTemplateList:
     for _, template := range list.Items {
       if template.GetName() != newInfraMachineTemplateName {
         outdatedTemplates = append(outdatedTemplates, &template)
       }
     }
   default:
     return nil, fmt.Errorf("%w: got unknown type %T", errUnexpectedInfraMachineTemplateListType, list)
   }
 
   return outdatedTemplates, nil
 }
pkg/controllers/machinesync/machine_sync_controller.go (4)

351-354: Avoid nil event object when mirroring (mapiMachine can be nil)

When creating the MAPI mirror, mapiMachine is nil. Recording an event on nil can panic. Use capiMachine as the event object when mapiMachine is nil.

 for _, warning := range warns {
   logger.Info("Warning during conversion", "warning", warning)
-  r.Recorder.Event(mapiMachine, corev1.EventTypeWarning, "ConversionWarning", warning)
+  // mapiMachine may be nil when we're creating a mirror; fall back to capiMachine.
+  var eventObj runtime.Object = mapiMachine
+  if eventObj == nil {
+    eventObj = capiMachine
+  }
+  r.Recorder.Event(eventObj, corev1.EventTypeWarning, "ConversionWarning", warning)
 }

1056-1064: Fix error handling: don’t return an error if RemoveFinalizer succeeds

Currently always returns a non‑nil error (even when err is nil). Check err and return nil on success.

   if capiMachine == nil && util.IsNilObject(infraMachine) {
     logger.Info("Cluster API machine and infra machine do not exist, removing corresponding Machine API machine sync finalizer")
     // We don't have  a capi machine or infra resouorces to clean up we can
     // just let the MAPI operators function as normal, and remove our own sync
     // finalizer.
-    _, err := util.RemoveFinalizer(ctx, r.Client, mapiMachine, SyncFinalizer)
-
-    return false, fmt.Errorf("failed to remove finalizer: %w", err)
+    if _, err := util.RemoveFinalizer(ctx, r.Client, mapiMachine, SyncFinalizer); err != nil {
+      return false, fmt.Errorf("failed to remove finalizer: %w", err)
+    }
+    return false, nil
   }

1177-1184: Fix error handling in CAPI→MAPI deletion (nil error wrap)

Same issue: only return error if RemoveFinalizer fails.

   if mapiMachine == nil {
     logger.Info("Machine API machine does not exist, removing corresponding Cluster API machine sync finalizer")
     // We don't have  a MAPI machine just let the CAPI operators function as
     // normal, and remove our own sync finalizer.
-    _, err := util.RemoveFinalizer(ctx, r.Client, capiMachine, SyncFinalizer)
-
-    return false, fmt.Errorf("failed to remove finalizer: %w", err)
+    if _, err := util.RemoveFinalizer(ctx, r.Client, capiMachine, SyncFinalizer); err != nil {
+      return false, fmt.Errorf("failed to remove finalizer: %w", err)
+    }
+    return false, nil
   }

1316-1360: Make compareMAPIMachines tolerant to non‑AWS providerSpecs; prefer Metal3 decode when available

Do not abort on AWS decode failure — try AWSProviderSpecFromRawExtension first, then BareMetalProviderSpecFromRawExtension (pkg/conversion/mapi2capi/metal3.go) and compare when available; if no provider decoder succeeds, skip the providerSpec diff and continue comparing Spec/Metadata. Mirror the same pattern in pkg/controllers/machinesetsync/machineset_sync_controller.go.

♻️ Duplicate comments (4)
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/localize/localize.go (1)

34-35: Critical: Buffer handling bug makes verification ineffective.

The past review comment is still valid. buildBuffer is passed by value to runBuildCmd (lines 74, 84), and runBuildCmd receives it by value (line 133). When buffer.Reset() executes (line 134), it only resets the local copy inside runBuildCmd, not the original buffer that buildCmd writes to. As a result:

  1. Both runBuildCmd calls read from empty local copies
  2. Both return empty strings
  3. Verification always "succeeds" ("" == "") even when builds differ

This completely breaks the verification feature.

Apply the fix from the previous review:

-	var buildBuffer bytes.Buffer
-	buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer)
+	var buildBuffer bytes.Buffer
+	buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer)
-			originalBuild, err := runBuildCmd(buildBuffer, buildCmd, args.target)
+			originalBuild, err := runBuildCmd(&buildBuffer, buildCmd, args.target)
-			localizedBuild, err := runBuildCmd(buildBuffer, buildCmd, buildDst)
+			localizedBuild, err := runBuildCmd(&buildBuffer, buildCmd, buildDst)
-func runBuildCmd(buffer bytes.Buffer, cmd *cobra.Command, folder string) (buildOutput string, err error) {
-	buffer.Reset()
+func runBuildCmd(buffer *bytes.Buffer, cmd *cobra.Command, folder string) (buildOutput string, err error) {
+	buffer.Reset()
-	return buffer.String(), nil
+	return buffer.String(), nil

Also applies to: 74-77, 84-87, 133-144

vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (1)

20-56: CRITICAL: Path traversal vulnerability confirmed (vendored code).

The path traversal vulnerability flagged in the previous review remains unaddressed. Using strings.TrimPrefix(path, src) at lines 28 and 37 can leave a leading path separator (e.g., when path="/abs/src/file" and src="/abs/src", the result is "/file"). When this result is passed to filepath.Join(dst, copyTo) at lines 41, 45, and 49, the absolute path causes dst to be ignored, allowing writes outside the destination directory.

Since this is vendored code from sigs.k8s.io/kustomize/kyaml/copyutil, the proper fix must come from upstream. However, given the severity:

  1. Immediate action: Check if a newer version of kustomize/kyaml has fixed this issue and update the vendor dependency.
  2. If no upstream fix exists: Consider reporting this to the kustomize project or applying a local patch with the filepath.Rel approach suggested in the previous review.

Verify the upstream kustomize version for this fix:

#!/bin/bash
# Check current vendored version and available updates
cd vendor/sigs.k8s.io/kustomize/kyaml
git log -1 --format="%H %s" 2>/dev/null || echo "Not a git submodule"

# Search for any existing issues or fixes in the repository
gh api graphql -f query='
{
  search(query: "repo:kubernetes-sigs/kustomize path:kyaml/copyutil TrimPrefix OR filepath.Rel", type: ISSUE, first: 10) {
    nodes {
      ... on Issue {
        title
        state
        url
        createdAt
      }
    }
  }
}'
pkg/conversion/capi2mapi/metal3.go (1)

90-99: Use the provider’s registered GroupVersion when populating TypeMeta.

Hard-coding baremetal.cluster.k8s.io/v1alpha1 leaves us with the wrong GVK for BareMetalMachineProviderSpec. Please drive this from the scheme (baremetalv1.GroupVersion.String()) so the serialized providerSpec carries the correct API version and stays aligned with future bumps.

-		TypeMeta: metav1.TypeMeta{
-			Kind:       "BareMetalMachineProviderSpec",
-			APIVersion: "baremetal.cluster.k8s.io/v1alpha1",
-		},
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "BareMetalMachineProviderSpec",
+			APIVersion: baremetalv1.GroupVersion.String(),
+		},
vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go (1)

755-757: Fix boundary regex: use blanklineStartRegex for the right segment

blankLine2 should test the start-of-blank-line on the right side to balance left/end vs right/start.

-  blankLine1 := lineBreak1 && blanklineEndRegex.MatchString(one)
-  blankLine2 := lineBreak2 && blanklineEndRegex.MatchString(two)
+  blankLine1 := lineBreak1 && blanklineEndRegex.MatchString(one)
+  blankLine2 := lineBreak2 && blanklineStartRegex.MatchString(two)
🧹 Nitpick comments (5)
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/localize/localize.go (2)

108-113: Minor: Inconsistent indentation in flag description.

The flag description string has inconsistent leading whitespace (tabs) that makes the formatting uneven. Consider cleaning up the indentation for better readability.

 	cmd.Flags().BoolVar(&f.noVerify,
 		"no-verify",
 		false,
-		`Does not verify that the outputs of kustomize build for target and newDir are the same after localization.
-		If not specified, this flag defaults to false and will run kustomize build.
-	`)
+		`Does not verify that the outputs of kustomize build for target and newDir are the same after localization.
+If not specified, this flag defaults to false and will run kustomize build.`)

136-140: Error guidance may confuse users in common scenarios.

The error message (lines 137-139) specifically mentions "executable permissions for downloaded exec binaries," which applies to plugin/helm scenarios. However, this message is shown for ANY build error, including common issues like syntax errors in kustomization.yaml or missing resources. Users encountering basic errors will see confusing guidance about exec permissions.

Consider either:

  1. Making the message more generic, or
  2. Conditionally showing exec-permission guidance only when the error suggests permission issues

Example of a more generic message:

 	buildErr := cmd.RunE(cmd, []string{folder})
 	if buildErr != nil {
-		log.Printf("If your target directory requires flags to build: \n"+
-			"1. Add executable permissions for the downloaded exec binaries in '%s'. \n"+
-			"2. Run kustomize build with the necessary flags and self-verify the outputs.", folder)
+		log.Printf("Build failed for '%s'. If your target requires special flags or uses plugins/helm charts, "+
+			"you may need to run kustomize build manually with appropriate flags to verify the localization.", folder)
 		return "", errors.Wrap(buildErr)
 	}
vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (2)

58-131: Refactor path handling for robustness.

The double strings.TrimPrefix pattern at lines 75 and 94 is fragile and can produce incorrect results in edge cases (e.g., when sourceDir="/path" and a file is named "/pathological", the first TrimPrefix produces "ological" instead of failing). While this doesn't create a security vulnerability (no write operations), it can cause incorrect diff results.

For consistency with the recommended fix in CopyDir and to avoid subtle path bugs, use filepath.Rel to compute relative paths:

 	// get set of filenames in the package source
 	upstreamFiles := sets.String{}
 	err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
 		if err != nil {
 			return err
 		}
 
 		// skip git repo if it exists
 		if IsDotGitFolder(path) {
 			return nil
 		}
 
-		upstreamFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, sourceDir), string(filepath.Separator)))
+		relPath, err := filepath.Rel(sourceDir, path)
+		if err != nil {
+			return err
+		}
+		upstreamFiles.Insert(relPath)
 		return nil
 	})

Apply the same pattern for localFiles at lines 83-96:

 	// get set of filenames in the cloned package
 	localFiles := sets.String{}
 	err = filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error {
 		if err != nil {
 			return err
 		}
 
 		// skip git repo if it exists
 		if IsDotGitFolder(path) {
 			return nil
 		}
 
-		localFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, destDir), string(filepath.Separator)))
+		relPath, err := filepath.Rel(destDir, path)
+		if err != nil {
+			return err
+		}
+		localFiles.Insert(relPath)
 		return nil
 	})

190-197: LGTM with optional refinement.

The deletion logic correctly treats missing files as success, which is appropriate for a cleanup helper.

For slightly better clarity, consider checking specifically for os.ErrNotExist:

+import "errors"
+
 // deleteFile deletes file from path, returns no error if file doesn't exist
 func deleteFile(path string) error {
-	if _, err := os.Stat(path); err != nil {
-		// return nil if file doesn't exist
-		return nil
+	if _, err := os.Stat(path); err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			return nil
+		}
+		return err
 	}
 	return os.Remove(path)
 }
pkg/controllers/machinesetsync/machineset_sync_controller.go (1)

1465-1516: Make compareMAPIMachineSets tolerant to non-AWS and bare-metal providerSpecs
Wrap the AWS decode in an if err==nil block to skip non-AWS specs, then add an else if branch using mapi2capi.BareMetalProviderSpecFromRawExtension to diff bare-metal providerSpecs. If neither decode succeeds, fall back to best-effort by comparing Spec, Metadata, and Status only.

📜 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

📥 Commits

Reviewing files that changed from the base of the PR and between adcf11c and b02dfe1.

⛔ Files ignored due to path filters (4)
  • e2e/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
  • go.work.sum is excluded by !**/*.sum
  • hack/tools/go.sum is excluded by !**/*.sum
📒 Files selected for processing (45)
  • cmd/machine-api-migration/main.go (3 hunks)
  • e2e/go.mod (1 hunks)
  • go.mod (2 hunks)
  • hack/tools/go.mod (2 hunks)
  • pkg/controllers/common.go (3 hunks)
  • pkg/controllers/machinesetsync/machineset_sync_controller.go (7 hunks)
  • pkg/controllers/machinesync/machine_sync_controller.go (6 hunks)
  • pkg/conversion/capi2mapi/metal3.go (1 hunks)
  • pkg/conversion/mapi2capi/metal3.go (1 hunks)
  • vendor/github.com/sergi/go-diff/AUTHORS (1 hunks)
  • vendor/github.com/sergi/go-diff/CONTRIBUTORS (1 hunks)
  • vendor/github.com/sergi/go-diff/LICENSE (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/match.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go (1 hunks)
  • vendor/modules.txt (7 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/grep.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/api/docs.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/build.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagenablehelm.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagsforfunctions.go (0 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/create/create.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbase.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbuildmetadata.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addcomponent.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addgenerator.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addmetadata.go (5 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addresource.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addtransformer.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/fix/convert.go (3 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removebuildmetadata.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeconfigmap.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removemetadata.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeresource.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removesecret.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removetransformer.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile/kustomizationfile.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/util/validate.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/localize/localize.go (6 hunks)
  • vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (1 hunks)
💤 Files with no reviewable changes (1)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagsforfunctions.go
✅ Files skipped from review due to trivial changes (1)
  • vendor/github.com/sergi/go-diff/LICENSE
🚧 Files skipped from review as they are similar to previous changes (15)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addresource.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbuildmetadata.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/build.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/create/create.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagenablehelm.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removemetadata.go
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/grep.go
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile/kustomizationfile.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addcomponent.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removetransformer.go
  • vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/api/docs.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/util/validate.go
  • vendor/modules.txt
🔇 Additional comments (36)
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removebuildmetadata.go (1)

7-7: Vendored dependency update: standard library modernization.

This change is part of an upstream kustomize dependency update that modernizes the code to use Go's standard library slices.Contains instead of a custom helper function. The logic is equivalent and correct.

Also applies to: 64-64

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addgenerator.go (1)

9-9: Vendored kustomize updated upstream
Bulk replacements of kustfile.StringInSlice with slices.Contains occur across multiple vendored kustomize files, indicating an upstream kustomize v5 vendor update. No manual modifications are needed on this file.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removesecret.go (1)

10-10: Do not manually modify vendored code
Vendored files must match upstream exactly. Confirm whether removesecret.go’s import of "slices" and the switch to slices.Contains exist in the kustomize/v5 upstream repository. If they do, bump the kustomize version in go.mod and re-vendor; if not, revert these edits.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addtransformer.go (1)

9-9: Ignore intentional vendor update
Changes in vendor/sigs.k8s.io/kustomize/.../addtransformer.go are from the vendoring commit (b02dfe1) migrating to Go’s slices.Contains. No action required.

Likely an incorrect or invalid review comment.

vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (3)

133-143: LGTM!

The .git detection logic is correct and handles nested paths properly. The use of filepath.Clean and filepath.ToSlash ensures consistent path normalization before checking components.


145-152: LGTM!

Standard and correct usage of the diffmatchpatch library for line-based diff generation.


154-188: LGTM!

The file synchronization logic is correct:

  • Handles missing source files by cleaning up the destination
  • Preserves destination permissions when present (reasonable default for sync operations)
  • Properly propagates errors from all file system operations
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeconfigmap.go (1)

10-10: Confirm upstream origin of vendored change.
This replaces kustfile.StringInSlice with slices.Contains in a vendored file. Please confirm:

  • Which upstream kustomize version or commit includes this refactor?
  • If it’s a manual patch, why modify vendored code instead of updating the dependency?
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeresource.go (1)

9-9: Confirm vendored update is upstream
Verify that go.mod’s require sigs.k8s.io/kustomize/kustomize/v5 v5.x.x matches this vendor change and rerun go mod vendor to ensure the slice import and slices.Contains usage came from an upstream kustomize/v5 release.

vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (3)

251-253: Validation logic correctly updated for Star removal.

The condition now properly requires either --image or --enable-exec with --exec-path when function arguments are provided (indicated by ArgsLenAtDash >= 0). This is consistent with removing Star-based function support.

However, this change is in a vendored file. Ensure this modification aligns with an upstream kustomize update rather than a manual edit.


1-314: Clarify the kustomize vendor update in this PR.

Changes under vendor/sigs.k8s.io/kustomize (run-fns.go, commit b02dfe1) appear to be a kustomize vendoring update with no corresponding go.mod version bump, yet the PR scope is “METAL-1583: baremetal: implement MAPI/CAPI migration.”
• Please extract this vendoring change into a standalone PR or justify its inclusion here.


23-23: Vendored kustomize update is intentional; no manual edits in run-fns.go. go mod verify passes and git log shows a “tidy, verify, and vendor” refresh commit.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/fix/convert.go (3)

59-63: Functional change is correct.

The replacement of stringInSlice with slices.Contains is semantically equivalent and correct. The logic continues to skip files that are not kustomization files.

Note: See the earlier comment regarding modification of vendored files.


227-227: LGTM!

The replacement of indexOf with slices.Index is correct. Both return -1 when the element is not found, and the existing error handling at lines 228-231 correctly handles this case.


10-10: No action needed: Go version ≥1.21 confirmed.

All modules specify Go ≥1.21, so the slices import is supported.

go.mod (3)

19-19: LGTM: Pseudo-version format is correct.

The pseudo-version for cluster-api-provider-baremetal follows Go's pseudo-version format (v0.0.0-timestamp-commit). This is appropriate for referencing a specific commit.


308-308: Ensure replace directive version matches the require directive.

The replace directive pins metal3-io/cluster-api-provider-metal3/api to v1.10.1, matching line 13. However, if v1.10.1 doesn't exist (as suggested by learnings), both lines will need correction once the correct version is identified.


13-13: No action required – Metal3 API v1.10.1 tag confirmed The api/v1.10.1 tag exists in the metal3-io/cluster-api-provider-metal3 repository.

hack/tools/go.mod (2)

11-11: LGTM: Kustomize version update.

The update from v5.4.1 to v5.6.0 is a minor version bump. This aligns with the broader Metal3 integration and ensures tooling compatibility.


216-216: LGTM: Indirect dependency update aligns with kustomize.

The update of sigs.k8s.io/kustomize/cmd/config from v0.14.0 to v0.19.0 is expected when upgrading kustomize to v5.6.0.

cmd/machine-api-migration/main.go (3)

24-24: LGTM: Metal3 import added.

The import of metal3v1 follows the established pattern for other cloud providers (AWS, OpenStack) and is necessary for Bare Metal platform support.


72-72: LGTM: Metal3 scheme registration.

The scheme registration follows the same pattern as other providers (lines 71, 73-74) and correctly uses utilruntime.Must for error handling.


201-201: LGTM: Bare Metal platform type added to switch.

The addition of configv1.BareMetalPlatformType correctly extends the controller startup logic to support Bare Metal clusters alongside AWS and OpenStack.

vendor/github.com/sergi/go-diff/AUTHORS (1)

1-26: LGTM: Standard vendor metadata file.

This AUTHORS file follows the standard format for listing copyright holders in a vendored Go library. The content is appropriate for vendor documentation.

vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go (1)

11-23: LGTM: Standard min/max helper functions.

These unexported utility functions are correctly implemented and are part of the vendored go-diff library. While Go 1.21+ includes built-in min/max functions, this vendored code maintains its own implementation for compatibility.

e2e/go.mod (1)

14-14: LGTM: Consistent dependency update.

The update of cluster-api-provider-baremetal pseudo-version aligns with the root go.mod (line 19), ensuring consistency across all modules in the repository.

vendor/github.com/sergi/go-diff/CONTRIBUTORS (1)

1-33: LGTM: Standard vendor metadata file.

This CONTRIBUTORS file follows the standard format for listing contributors to a vendored Go library. The distinction from AUTHORS (copyright holders vs. contributors) is clearly documented in the header.

pkg/controllers/common.go (3)

22-22: LGTM: Metal3 import added.

The import of metal3v1 is necessary for Bare Metal platform support and follows the established pattern for other cloud providers in this file.


46-47: LGTM: Bare Metal case added for Machine and Cluster initialization.

The addition correctly returns Metal3Machine and Metal3Cluster for Bare Metal platforms, following the same pattern as AWS (lines 44-45) and OpenStack (lines 48-49).


65-66: LGTM: Bare Metal case added for MachineTemplate and Cluster initialization.

The addition correctly returns Metal3MachineTemplate and Metal3Cluster for Bare Metal platforms, following the same pattern as other providers in this function.

pkg/controllers/machinesetsync/machineset_sync_controller.go (3)

674-688: Metal3 CAPI→MAPI MachineSet conversion: LGTM

Type assertions and delegation to Metal3 converter look correct.


726-728: Metal3 MAPI→CAPI MachineSet conversion: LGTM

Constructor path mirrors other providers and returns MachineSet + MachineTemplate.


1331-1332: Metal3 lists initializer: LGTM

Returning Metal3MachineTemplateList and Metal3ClusterList is correct.

pkg/controllers/machinesync/machine_sync_controller.go (3)

566-568: Metal3 MAPI→CAPI Machine conversion: LGTM


592-603: Metal3 CAPI→MAPI Machine conversion: LGTM


1389-1406: Metal3 infra-machine compare: LGTM

Spec and metadata diffs mirror other providers.

Comment on lines +1371 to +1387
typedInfraMachineTemplate1, ok := infraMachineTemplate1.(*metal3v1.Metal3MachineTemplate)
if !ok {
return nil, errAssertingCAPIMetal3MachineTemplate
}

typedinfraMachineTemplate2, ok := infraMachineTemplate2.(*metal3v1.Metal3MachineTemplate)
if !ok {
return nil, errAssertingCAPIMetal3MachineTemplate
}

diff := make(map[string]any)

if diffSpec := deep.Equal(typedInfraMachineTemplate1.Spec, typedinfraMachineTemplate2.Spec); len(diffSpec) > 0 {
diff[".spec"] = diffSpec
}

if diffObjectMeta := util.ObjectMetaEqual(typedInfraMachineTemplate1.ObjectMeta, typedinfraMachineTemplate2.ObjectMeta); len(diffObjectMeta) > 0 {
diff[".metadata"] = diffObjectMeta
}

// TODO: Evaluate if we want to add status comparison if needed in the future (e.g. for scale from zero capacity).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix OpenStack error constant; consider consistent metadata diff

  • Line 1397: On OpenStack, wrong error constant is returned on type assertion failure (IBMPowerVS instead of OpenStack). Use errAssertingCAPIOpenStackMachineTemplate.
  • Optional: Use util.ObjectMetaEqual for metadata like other providers to avoid noisy diffs on transient fields.
 case configv1.OpenStackPlatformType:
   typedInfraMachineTemplate1, ok := infraMachineTemplate1.(*openstackv1.OpenStackMachineTemplate)
   if !ok {
-    return nil, errAssertingCAPIIBMPowerVSMachineTemplate
+    return nil, errAssertingCAPIOpenStackMachineTemplate
   }
 
   typedinfraMachineTemplate2, ok := infraMachineTemplate2.(*openstackv1.OpenStackMachineTemplate)
   if !ok {
     return nil, errAssertingCAPIOpenStackMachineTemplate
   }
 
   diff := make(map[string]any)
 
   if diffSpec := deep.Equal(typedInfraMachineTemplate1.Spec, typedinfraMachineTemplate2.Spec); len(diffSpec) > 0 {
     diff[".spec"] = diffSpec
   }
 
-  if diffObjectMeta := deep.Equal(typedInfraMachineTemplate1.ObjectMeta, typedinfraMachineTemplate2.ObjectMeta); len(diffObjectMeta) > 0 {
+  // Optional: keep consistent with other providers
+  if diffObjectMeta := util.ObjectMetaEqual(typedInfraMachineTemplate1.ObjectMeta, typedinfraMachineTemplate2.ObjectMeta); len(diffObjectMeta) > 0 {
     diff[".metadata"] = diffObjectMeta
   }

Also applies to: 1395-1403

🤖 Prompt for AI Agents
In pkg/controllers/machinesetsync/machineset_sync_controller.go around lines
1371-1392 (also apply to 1395-1403), fix the incorrect error constant used on
the type-assertion failure for the OpenStack provider by returning
errAssertingCAPIOpenStackMachineTemplate instead of the
IBMPowerVS/OpenStack-mismatched constant; additionally, make the metadata
comparison use util.ObjectMetaEqual (same approach as spec) to avoid noisy diffs
from transient metadata fields.

Comment on lines +121 to +130
if m.machine == nil || m.metal3Machine == nil || m.metal3Cluster == nil {
return nil, nil, errCAPIMachineMetal3MachineMetal3ClusterCannotBeNil
}

var (
errors field.ErrorList
warnings []string
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard infrastructure access and initialize labels before writing.

m.infrastructure can be nil, so m.infrastructure.Status.InfrastructureName will panic. The same block also writes into capiMachine.Labels without ensuring the map exists. Both crash the conversion path.

-	if m.infrastructure == nil || m.infrastructure.Status.InfrastructureName == "" {
-		errs = append(errs, field.Invalid(field.NewPath("infrastructure", "status", "infrastructureName"), m.infrastructure.Status.InfrastructureName, "infrastructure cannot be nil and infrastructure.Status.InfrastructureName cannot be empty"))
-	} else {
-		capiMachine.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
-		capiMachine.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
-	}
+	clusterNamePath := field.NewPath("infrastructure", "status", "infrastructureName")
+	switch {
+	case m.infrastructure == nil:
+		errs = append(errs, field.Invalid(clusterNamePath, nil, "infrastructure cannot be nil"))
+	case m.infrastructure.Status.InfrastructureName == "":
+		errs = append(errs, field.Invalid(clusterNamePath, "", "infrastructure.Status.InfrastructureName cannot be empty"))
+	default:
+		capiMachine.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
+		if capiMachine.Labels == nil {
+			capiMachine.Labels = map[string]string{}
+		}
+		capiMachine.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pkg/conversion/capi2mapi/metal3.go around lines 121 to 128, guard against
m.infrastructure being nil before accessing
m.infrastructure.Status.InfrastructureName to avoid a nil-pointer panic, and
ensure capiMachine.Labels is initialized (create the map if nil) before writing
into it; update the conversion logic to check m.infrastructure != nil then read
InfrastructureName, and right before assigning labels ensure capiMachine.Labels
= make(map[string]string) if it is nil.

Comment on lines +186 to +194

mapiMachineSet.Spec.Template.Spec = mapiMachine.Spec

// Copy the labels and annotations from the Machine to the template.
mapiMachineSet.Spec.Template.ObjectMeta.Annotations = mapiMachine.ObjectMeta.Annotations
mapiMachineSet.Spec.Template.ObjectMeta.Labels = mapiMachine.ObjectMeta.Labels

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same nil-dereference risk in MachineSet path (and need label map init).

m.infrastructure may be nil, and capiMachineSet.Labels can be nil. This mirrors the panic above; please apply the same guard/initialization here.

-	if m.infrastructure == nil || m.infrastructure.Status.InfrastructureName == "" {
-		errs = append(errs, field.Invalid(field.NewPath("infrastructure", "status", "infrastructureName"), m.infrastructure.Status.InfrastructureName, "infrastructure cannot be nil and infrastructure.Status.InfrastructureName cannot be empty"))
-	} else {
-		capiMachineSet.Spec.Template.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
-		capiMachineSet.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
-		capiMachineSet.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
-	}
+	clusterNamePath := field.NewPath("infrastructure", "status", "infrastructureName")
+	switch {
+	case m.infrastructure == nil:
+		errs = append(errs, field.Invalid(clusterNamePath, nil, "infrastructure cannot be nil"))
+	case m.infrastructure.Status.InfrastructureName == "":
+		errs = append(errs, field.Invalid(clusterNamePath, "", "infrastructure.Status.InfrastructureName cannot be empty"))
+	default:
+		capiMachineSet.Spec.Template.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
+		capiMachineSet.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
+		if capiMachineSet.Labels == nil {
+			capiMachineSet.Labels = map[string]string{}
+		}
+		capiMachineSet.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
+	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pkg/conversion/capi2mapi/metal3.go around lines 186-192, avoid
nil-dereference when copying infrastructure and labels: first guard accesses to
m.infrastructure (only use m.infrastructure fields if m.infrastructure != nil),
and ensure destination and source label maps are initialized before assignment
(if capiMachineSet.Labels is nil, create an empty map on the
template.ObjectMeta.Labels or copy after initializing template labels map). Also
defensively handle annotations similarly (initialize target map if nil) so
assignments won’t panic.

"bytes"
"fmt"
"path"
"slices"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

❓ Verification inconclusive

Modifying vendored dependencies creates maintenance risk.

This file is in vendor/ and should typically mirror upstream kustomize code. Direct modifications to vendored files:

  • Create merge conflicts when updating dependencies
  • Diverge from upstream behavior
  • Are lost if the vendor directory is regenerated

Additionally, the PR objectives mention Metal3/BareMetal migration (METAL-1583), but this kustomize file appears unrelated to that work.

Please verify:

  1. Is this modification intentional and necessary for the baremetal migration?
  2. Should this change be contributed upstream to kustomize instead?
  3. Are the removed helper functions (stringInSlice, indexOf) used elsewhere in the codebase?

Run this script to check for usage of the removed helpers:


🏁 Script executed:

#!/bin/bash
# Check if stringInSlice or indexOf are used elsewhere in the codebase
rg -n --type go -C 3 '\b(stringInSlice|indexOf)\s*\(' -g '!vendor/**'

Length of output: 148


🏁 Script executed:

#!/bin/bash
grep -RIn --exclude-dir=vendor --include='*.go' -E '\b(stringInSlice|indexOf)\s*\(' .

Length of output: 0


Do not modify vendored kustomize code directly.

Revert all edits in vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/fix/convert.go. Vendored files must mirror upstream; if slices-based helpers are needed, contribute them upstream and then update the vendor. Removed helpers (stringInSlice, indexOf) are not used elsewhere.

🤖 Prompt for AI Agents
In vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/fix/convert.go around
line 10, the file was edited to add "slices" and removed helper functions
(stringInSlice, indexOf); do not modify vendored code — revert this file to
match upstream exactly (restore the original content and helpers if they existed
upstream), remove any local uses of the newly added slices import, and if
slices-based helpers are required, create a proper upstream PR to add them and
then update vendor via go mod vendor rather than editing vendored files
directly.

@honza
Copy link
Member Author

honza commented Oct 3, 2025

/test unit

@openshift-ci-robot
Copy link

openshift-ci-robot commented Oct 29, 2025

@honza: This pull request references METAL-1583 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:

Assisted-by: claude-4.5-sonnet

Summary by CodeRabbit

  • New Features
  • Full Bare Metal (Metal3) platform support for migration and sync flows, including bidirectional Machine/MachineSet conversions and controller enablement.
  • Improvements
  • More accurate diff/compare behavior (including infra status) and expanded conversion diagnostics; new debug/verification flags for build/localize and Helm debug support.
  • Chores
  • Updated dependencies and vendor additions (diff library, license/contributors, kustomize updates) and assorted tooling/version bumps.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addmetadata.go (1)

1-215: Revert vendor file changes and use proper Go modules workflow.

This is a vendored third-party file from kustomize. When go.mod changes, go.mod and vendor/modules.txt must remain in sync, and go mod vendor should be run to update the vendor directory—not the reverse.

The script output confirms: kustomize files in vendor were modified, but go.mod shows no corresponding version update to kustomize. This means these are manual edits that will be lost on the next go mod tidy && go mod vendor run.

To fix this, choose one approach:

  1. If these changes should be permanent: Update the kustomize dependency version in go.mod to one that includes this functionality, then run go mod tidy && go mod vendor.

  2. If this is a temporary local patch: Add a replace directive to go.mod (e.g., replace sigs.k8s.io/kustomize => ./path/to/local/kustomize) rather than modifying vendor directly. When finished working, remove the replace directive from go.mod.

For now, revert all changes to files under vendor/ and implement one of the above approaches instead.

vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (1)

1-313: Now I understand the context better. Let me examine the actual code issues in detail:

Now I'll examine the specific code logic to understand the incomplete removal:

Based on my analysis, the vendored dependency update IS relevant to the PR (kustomize is used for manifest generation in the codebase), but there are genuine code quality issues to address. Let me generate the rewritten review comment:


Remove dead code from incomplete starlark support removal in run-fns.go.

Starlark support for KRM functions was removed to cleanup dependencies, as this feature was deprecated 3 years ago. However, the refactoring is incomplete:

  • StarPath, StarURL, StarName fields remain in the RunFnRunner struct (lines 86-88) but have no corresponding CLI flags to populate them
  • The condition at line 108 checks these fields (r.Image == "" && r.StarPath == "" && r.StarURL == "" && r.ExecPath == "") but they can never be non-empty, making this branch partially dead code
  • The validation at line 251 was updated to remove star field checks, but the struct fields and condition still reference them

Remove the unused struct fields and update the condition at line 108 to reflect the removal of starlark support, leaving only: if r.Image == "" && r.ExecPath == "".

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/localize/localize.go (1)

1-144: Vendored file contains custom modifications with buffer logic bug; changes will be overwritten on dependency updates.

Two critical issues:

  1. Custom modification to vendored code: The noVerify flag and verification logic (lines 45-94) do not exist in the upstream kustomize v5.6.0 (confirmed via git history search). These changes are custom patches that will be lost when the kustomize dependency is next updated.

  2. Buffer pass-by-value bug breaks verification (lines 74, 84, 133-144): The buildBuffer is declared as a value type (line 34), but buildCmd writes to it via pointer (&buildBuffer, line 35). However, both calls to runBuildCmd() pass the buffer by value (lines 74, 84), creating copies. Inside runBuildCmd(), buffer.Reset() operates on the copy, not the original, so verification always reads an empty string. This causes the comparison to always succeed incorrectly (both empty outputs = identical).

Remove custom modifications from vendor/ and instead propose changes to the actual kustomize upstream repository if this verification is needed.

pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go (1)

352-416: Missing BareMetal status handling in setChangedCAPIInfraMachineStatusFields

compareCAPIInfraMachines supports BareMetal, but setChangedCAPIInfraMachineStatusFields has no BareMetal branch. Status updates for Metal3Machine will fail. Add Metal3 handling like AWS/OpenStack.

Apply:

 func setChangedCAPIInfraMachineStatusFields(platform configv1.PlatformType, existingCAPIInfraMachine, convertedCAPIInfraMachine client.Object) error {
   switch platform {
+  case configv1.BareMetalPlatformType:
+    existing, ok := existingCAPIInfraMachine.(*metal3v1.Metal3Machine)
+    if !ok {
+      return errAssertingCAPIMetal3Machine
+    }
+    converted, ok := convertedCAPIInfraMachine.(*metal3v1.Metal3Machine)
+    if !ok {
+      return errAssertingCAPIMetal3Machine
+    }
+    util.EnsureCAPIConditions(existing, converted)
+    // No v1beta2-specific conditions for Metal3Machine today.
+    existing.Status = converted.Status
+    return nil
   case configv1.AWSPlatformType:
pkg/controllers/machinesetsync/machineset_sync_controller.go (2)

367-394: Outdated template cleanup misses Metal3 lists

filterOutdatedInfraMachineTemplates doesn’t handle Metal3MachineTemplateList; BareMetal cleanup will error.

Add Metal3 case:

 func filterOutdatedInfraMachineTemplates(infraMachineTemplateList client.ObjectList, newInfraMachineTemplateName string) ([]client.Object, error) {
   outdatedTemplates := []client.Object{}
   switch list := infraMachineTemplateList.(type) {
   case *awsv1.AWSMachineTemplateList:
     ...
+  case *metal3v1.Metal3MachineTemplateList:
+    for _, template := range list.Items {
+      if template.GetName() != newInfraMachineTemplateName {
+        outdatedTemplates = append(outdatedTemplates, &template)
+      }
+    }
   case *ibmpowervsv1.IBMPowerVSMachineTemplateList:
     ...
   case *openstackv1.OpenStackMachineTemplateList:
     ...
   default:
     return nil, fmt.Errorf("%w: got unknown type %T", errUnexpectedInfraMachineTemplateListType, list)
   }

1039-1048: Make compareMAPIMachineSets provider-aware (breaks on BareMetal today)

compareMAPIMachineSets always parses AWS providerSpec; BareMetal (and others) will fail on CAPI→MAPI flow.

Apply minimal change to pass platform and parse accordingly:

- func compareMAPIMachineSets(a, b *mapiv1beta1.MachineSet) (map[string]any, error) {
+ func compareMAPIMachineSets(platform configv1.PlatformType, a, b *mapiv1beta1.MachineSet) (map[string]any, error) {
   diff := make(map[string]any)
-
- ps1, err := mapi2capi.AWSProviderSpecFromRawExtension(a.Spec.Template.Spec.ProviderSpec.Value)
+ switch platform {
+ case configv1.AWSPlatformType:
+   ps1, err := mapi2capi.AWSProviderSpecFromRawExtension(a.Spec.Template.Spec.ProviderSpec.Value)
   if err != nil {
     return nil, fmt.Errorf("unable to parse first MAPI machine set providerSpec: %w", err)
   }
-
- ps2, err := mapi2capi.AWSProviderSpecFromRawExtension(b.Spec.Template.Spec.ProviderSpec.Value)
+   ps2, err := mapi2capi.AWSProviderSpecFromRawExtension(b.Spec.Template.Spec.ProviderSpec.Value)
   if err != nil {
     return nil, fmt.Errorf("unable to parse second MAPI machine set providerSpec: %w", err)
   }
-
- // Sort the tags by name...
- sort.Slice(ps1.Tags, func(i, j int) bool { return ps1.Tags[i].Name < ps1.Tags[j].Name })
- sort.Slice(ps2.Tags, func(i, j int) bool { return ps2.Tags[i].Name < ps2.Tags[j].Name })
-
- if diffProviderSpec := deep.Equal(ps1, ps2); len(diffProviderSpec) > 0 {
-   diff[".providerSpec"] = diffProviderSpec
- }
+   sort.Slice(ps1.Tags, func(i, j int) bool { return ps1.Tags[i].Name < ps1.Tags[j].Name })
+   sort.Slice(ps2.Tags, func(i, j int) bool { return ps2.Tags[i].Name < ps2.Tags[j].Name })
+   if diffProviderSpec := deep.Equal(ps1, ps2); len(diffProviderSpec) > 0 {
+     diff[".providerSpec"] = diffProviderSpec
+   }
+ case configv1.BareMetalPlatformType:
+   bm1, err := mapi2capi.BareMetalProviderSpecFromRawExtension(a.Spec.Template.Spec.ProviderSpec.Value)
+   if err != nil {
+     return nil, fmt.Errorf("unable to parse first MAPI machine set providerSpec: %w", err)
+   }
+   bm2, err := mapi2capi.BareMetalProviderSpecFromRawExtension(b.Spec.Template.Spec.ProviderSpec.Value)
+   if err != nil {
+     return nil, fmt.Errorf("unable to parse second MAPI machine set providerSpec: %w", err)
+   }
+   if diffProviderSpec := deep.Equal(bm1, bm2); len(diffProviderSpec) > 0 {
+     diff[".providerSpec"] = diffProviderSpec
+   }
+ default:
+   // Fallback: compare raw providerSpec payloads for other providers
+   if diffProviderSpec := deep.Equal(a.Spec.Template.Spec.ProviderSpec.Value, b.Spec.Template.Spec.ProviderSpec.Value); len(diffProviderSpec) > 0 {
+     diff[".providerSpec"] = diffProviderSpec
+   }
+ }
   // Remove the providerSpec from the Spec as we've already compared them.
   aCopy := a.DeepCopy()
   aCopy.Spec.Template.Spec.ProviderSpec.Value = nil
   bCopy := b.DeepCopy()
   bCopy.Spec.Template.Spec.ProviderSpec.Value = nil
   ...
   return diff, nil
 }

And update call site:

- mapiMachineSetsDiff, err := compareMAPIMachineSets(existingMAPIMachineSet, convertedMAPIMachineSet)
+ mapiMachineSetsDiff, err := compareMAPIMachineSets(r.Platform, existingMAPIMachineSet, convertedMAPIMachineSet)

Also applies to: 1460-1511

♻️ Duplicate comments (5)
vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (1)

34-52: Critical path traversal vulnerability already flagged; verify vendored version and usage.

The path traversal issue using strings.TrimPrefix has been documented in previous reviews. Since this is vendored code, the recommended approach is to:

  1. Verify the version of sigs.k8s.io/kustomize/kyaml being vendored
  2. Check if a patched version exists upstream
  3. Verify whether CopyDir is actually invoked in the codebase

Run this script to check usage of the vulnerable CopyDir function:

#!/bin/bash
# Check if CopyDir from this vendored package is used anywhere in the codebase

echo "=== Searching for imports of copyutil package ==="
rg -n --type=go 'import.*copyutil|copyutil\.'

echo ""
echo "=== Searching for CopyDir function calls ==="
rg -n --type=go 'CopyDir\s*\(' -g '!vendor/**'

echo ""
echo "=== Checking vendored kustomize version ==="
fd -t f 'go.mod|go.sum' --exec grep -H 'sigs.k8s.io/kustomize/kyaml'
vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/localize/localize.go (1)

34-35: Critical: Buffer passed by value breaks verification.

The past review comment correctly identifies this critical bug. The buildBuffer is passed by value to runBuildCmd (lines 74, 84), but the build command writes to the original buffer (pointer passed on line 35). This means:

  1. runBuildCmd receives and resets a copy of the buffer (line 134)
  2. The build command writes to the original buffer
  3. runBuildCmd reads from the empty copy and returns empty string (line 143)
  4. Both originalBuild and localizedBuild are empty strings
  5. Verification always succeeds (line 89) even when outputs differ

This makes the entire verification feature non-functional.

Apply the fix from the previous review (or update the vendored dependency if this is fixed upstream):

-	var buildBuffer bytes.Buffer
-	buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer)
+	var buildBuffer bytes.Buffer
+	buildCmd := build.NewCmdBuild(fs, &build.Help{}, &buildBuffer)

-			originalBuild, err := runBuildCmd(buildBuffer, buildCmd, args.target)
+			originalBuild, err := runBuildCmd(&buildBuffer, buildCmd, args.target)

-			localizedBuild, err := runBuildCmd(buildBuffer, buildCmd, buildDst)
+			localizedBuild, err := runBuildCmd(&buildBuffer, buildCmd, buildDst)

-func runBuildCmd(buffer bytes.Buffer, cmd *cobra.Command, folder string) (buildOutput string, err error) {
+func runBuildCmd(buffer *bytes.Buffer, cmd *cobra.Command, folder string) (buildOutput string, err error) {

Also applies to: 74-74, 84-84, 133-144

pkg/controllers/machinesetsync/machineset_sync_controller.go (1)

1390-1411: Fix OpenStack assertion error and align metadata diff

Wrong constant on type assertion failure and inconsistent metadata diff.

Apply:

-    if !ok {
-      return nil, errAssertingCAPIIBMPowerVSMachineTemplate
-    }
+    if !ok {
+      return nil, errAssertingCAPIOpenStackMachineTemplate
+    }
...
-  if diffObjectMeta := deep.Equal(typedInfraMachineTemplate1.ObjectMeta, typedinfraMachineTemplate2.ObjectMeta); len(diffObjectMeta) > 0 {
+  if diffObjectMeta := util.ObjectMetaEqual(typedInfraMachineTemplate1.ObjectMeta, typedinfraMachineTemplate2.ObjectMeta); len(diffObjectMeta) > 0 {
     diff[".metadata"] = diffObjectMeta
   }
pkg/conversion/mapi2capi/metal3.go (2)

121-127: Nil-deref and missing label map init

Avoid dereferencing nil infrastructure and ensure Labels map is initialized.

Apply:

- if m.infrastructure == nil || m.infrastructure.Status.InfrastructureName == "" {
-   errs = append(errs, field.Invalid(field.NewPath("infrastructure", "status", "infrastructureName"), m.infrastructure.Status.InfrastructureName, "infrastructure cannot be nil and infrastructure.Status.InfrastructureName cannot be empty"))
- } else {
-   capiMachine.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
-   capiMachine.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
- }
+ clusterNamePath := field.NewPath("infrastructure", "status", "infrastructureName")
+ switch {
+ case m.infrastructure == nil:
+   errs = append(errs, field.Invalid(clusterNamePath, nil, "infrastructure cannot be nil"))
+ case m.infrastructure.Status.InfrastructureName == "":
+   errs = append(errs, field.Invalid(clusterNamePath, "", "infrastructure.Status.InfrastructureName cannot be empty"))
+ default:
+   capiMachine.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
+   if capiMachine.Labels == nil {
+     capiMachine.Labels = map[string]string{}
+   }
+   capiMachine.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
+ }

186-192: Same infra nil-guard and label map init for MachineSet path

Mirror the fix for MachineSet cluster/labels.

Apply:

- if m.infrastructure == nil || m.infrastructure.Status.InfrastructureName == "" {
-   errs = append(errs, field.Invalid(field.NewPath("infrastructure", "status", "infrastructureName"), m.infrastructure.Status.InfrastructureName, "infrastructure cannot be nil and infrastructure.Status.InfrastructureName cannot be empty"))
- } else {
-   capiMachineSet.Spec.Template.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
-   capiMachineSet.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
-   capiMachineSet.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
- }
+ clusterNamePath := field.NewPath("infrastructure", "status", "infrastructureName")
+ switch {
+ case m.infrastructure == nil:
+   errs = append(errs, field.Invalid(clusterNamePath, nil, "infrastructure cannot be nil"))
+ case m.infrastructure.Status.InfrastructureName == "":
+   errs = append(errs, field.Invalid(clusterNamePath, "", "infrastructure.Status.InfrastructureName cannot be empty"))
+ default:
+   capiMachineSet.Spec.Template.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
+   capiMachineSet.Spec.ClusterName = m.infrastructure.Status.InfrastructureName
+   if capiMachineSet.Labels == nil {
+     capiMachineSet.Labels = map[string]string{}
+   }
+   capiMachineSet.Labels[clusterv1.ClusterNameLabel] = m.infrastructure.Status.InfrastructureName
+ }
🧹 Nitpick comments (2)
vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (2)

86-88: Dead code: Star-related fields are never populated.

The StarPath, StarURL, and StarName fields are defined but never set through CLI flags (removed in the refactoring). They remain as dead code that serves no purpose.

Consider removing these unused fields to complete the star functionality removal:

 	Image              string
-	StarPath           string
-	StarURL            string
-	StarName           string
 	EnableExec         bool

108-110: Dead code: Star field checks in condition are always true.

Since StarPath and StarURL are never populated (no CLI flags set them), the checks r.StarPath == "" and r.StarURL == "" are always true. This makes these condition parts meaningless.

Simplify the condition to remove dead checks:

-	if r.Image == "" && r.StarPath == "" && r.ExecPath == "" && r.StarURL == "" {
+	if r.Image == "" && r.ExecPath == "" {
 		return nil, 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

📥 Commits

Reviewing files that changed from the base of the PR and between b02dfe1 and 5cb6da1.

⛔ Files ignored due to path filters (4)
  • e2e/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
  • go.work.sum is excluded by !**/*.sum
  • hack/tools/go.sum is excluded by !**/*.sum
📒 Files selected for processing (46)
  • cmd/machine-api-migration/main.go (3 hunks)
  • e2e/go.mod (1 hunks)
  • go.mod (2 hunks)
  • hack/tools/go.mod (2 hunks)
  • pkg/controllers/common.go (3 hunks)
  • pkg/controllers/machinesetsync/machineset_sync_controller.go (7 hunks)
  • pkg/controllers/machinesync/machine_sync_controller.go (4 hunks)
  • pkg/controllers/machinesync/machine_sync_mapi2capi_infrastructure.go (3 hunks)
  • pkg/conversion/capi2mapi/metal3.go (1 hunks)
  • pkg/conversion/mapi2capi/metal3.go (1 hunks)
  • vendor/github.com/sergi/go-diff/AUTHORS (1 hunks)
  • vendor/github.com/sergi/go-diff/CONTRIBUTORS (1 hunks)
  • vendor/github.com/sergi/go-diff/LICENSE (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/match.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go (1 hunks)
  • vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go (1 hunks)
  • vendor/modules.txt (7 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/grep.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/api/docs.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/build.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagenablehelm.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagsforfunctions.go (0 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/create/create.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbase.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbuildmetadata.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addcomponent.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addgenerator.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addmetadata.go (5 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addresource.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addtransformer.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/fix/convert.go (3 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removebuildmetadata.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeconfigmap.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removemetadata.go (1 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeresource.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removesecret.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removetransformer.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile/kustomizationfile.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/util/validate.go (2 hunks)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/localize/localize.go (6 hunks)
  • vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go (1 hunks)
💤 Files with no reviewable changes (1)
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagsforfunctions.go
🚧 Files skipped from review as they are similar to previous changes (19)
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/grep.go
  • vendor/sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/api/docs.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addcomponent.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/build.go
  • pkg/controllers/common.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/util/validate.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeconfigmap.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removemetadata.go
  • cmd/machine-api-migration/main.go
  • vendor/github.com/sergi/go-diff/diffmatchpatch/stringutil.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile/kustomizationfile.go
  • vendor/github.com/sergi/go-diff/diffmatchpatch/diffmatchpatch.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/create/create.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addresource.go
  • e2e/go.mod
  • vendor/github.com/sergi/go-diff/LICENSE
  • vendor/github.com/sergi/go-diff/diffmatchpatch/mathutil.go
  • vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbase.go
  • pkg/controllers/machinesync/machine_sync_controller.go
🔇 Additional comments (15)
vendor/github.com/sergi/go-diff/AUTHORS (1)

1-25: Standard vendor attribution file.

This is a standard AUTHORS file from the upstream go-diff library containing copyright holders for legal/licensing purposes. As a vendored dependency file, it should remain unmodified from upstream.

vendor/github.com/sergi/go-diff/CONTRIBUTORS (1)

1-32: Standard vendor attribution file.

This is a standard CONTRIBUTORS file from the upstream go-diff library listing individual contributors. As a vendored dependency file, it should remain unmodified from upstream.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removetransformer.go (1)

8-8: LGTM! Vendor update adopts standard library.

The migration from kustfile.StringInSlice to slices.Contains is correct and aligns with modern Go practices. This appears to be an upstream kustomize change pulled in via a vendor update.

Also applies to: 76-76

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removesecret.go (1)

10-10: LGTM: Routine vendor dependency update.

This vendor file has been updated to use the standard library slices.Contains instead of a custom helper function. The logic is preserved and the argument order is correctly adjusted for the new API.

Also applies to: 93-93

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removeresource.go (1)

9-9: Now let me generate a script to check vendor/modules.txt and understand the workspace structure better:

Let me now check vendor/modules.txt and the module declarations to verify this is a legitimate vendor update:

Based on my comprehensive verification, I can now provide the rewritten review comment:

Verify vendor directory is in sync with go.mod/go.sum.

The vendor file contains changes consistent with kustomize v5.4.0's fix for 'golang.org/x/exp/slices' dependencies (migrating to stdlib slices package in Go 1.21+). However, the initial grep found no entry for sigs.k8s.io/kustomize/kustomize/v5 in go.mod, which suggests a potential sync issue.

Please confirm:

  1. Check vendor/modules.txt to verify kustomize/v5 is listed with the correct version
  2. Check go.sum for kustomize entries matching the vendor contents
  3. Ensure go work vendor (or GOWORK=off go mod vendor) has been run to regenerate vendor from current workspace dependencies
vendor/sigs.k8s.io/kustomize/cmd/config/internal/commands/run-fns.go (2)

251-253: Validation simplified correctly, but inconsistent with usage.

The validation correctly requires either --image or --exec-path, aligning with the removal of star functionality. However, line 108 still checks for StarPath and StarURL, creating an inconsistency between validation and usage logic.

Once the dead star field checks are removed from line 108 (as suggested in the previous comment), this validation will be fully consistent with the implementation.


23-23: LGTM: Documentation fix.

The comment now correctly reflects the function name GetRunFnRunner instead of the incorrect GetCatRunner.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/build/flagenablehelm.go (1)

34-38: Remove the review comment—it is based on incorrect assumptions.

This file was modified as part of a legitimate vendor operation (commit shows "tidy, verify, and vendor"), not a direct patch. The --helm-debug flag was officially added in upstream kustomize v5.5.0, and the commit's +5 lines to flagenablehelm.go represent the dependency upgrade being applied via go mod vendor—a standard Go workflow. The connection to Metal3/CAPI is valid: the Cluster CAPI Operator uses kustomize to manage manifests. No manual verification is needed; the code change is upstream and correct.

Likely an incorrect or invalid review comment.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addbuildmetadata.go (1)

64-68: Contains refactor keeps behavior intact

Nice cleanup—once the Go toolchain question is settled, this is a straight behavior-preserving swap.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/fix/convert.go (1)

227-228: Good move replacing the ad-hoc index helper

Switching to slices.Index drops bespoke helpers and relies on stdlib generics—once the Go version bump lands, this will be easier to maintain.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/remove/removebuildmetadata.go (1)

64-67: Filter logic unchanged

The switch to slices.Contains preserves the guard and reads a bit clearer—looks good once we’re on the newer Go toolchain.

vendor/github.com/sergi/go-diff/diffmatchpatch/operation_string.go (1)

1-17: Stringer output matches upstream

The generated Operation.String() aligns with the canonical go-diff bits and will aid logging/debugging. 👍

vendor/github.com/sergi/go-diff/diffmatchpatch/match.go (1)

17-160: Bitap match port looks correct

This is the upstream Bitap matcher verbatim, so the fuzzy search behavior should now line up with the reference implementation.

vendor/github.com/sergi/go-diff/diffmatchpatch/patch.go (1)

22-556: Patch subsystem matches upstream

Thanks for pulling in the full patch surface—this mirrors the upstream go-diff implementation and should behave identically.

vendor/sigs.k8s.io/kustomize/kustomize/v5/commands/edit/add/addgenerator.go (1)

9-9: No action required—Go toolchain supports stdlib slices

The codebase compiles with Go 1.24.0, which fully supports the slices package added in this change. The stdlib slices requirement (≥1.21) is satisfied. The import is compatible and safe to merge.

Comment on lines +91 to +96
mapaProviderConfig := baremetalv1.BareMetalMachineProviderSpec{
TypeMeta: metav1.TypeMeta{
Kind: "BareMetalMachineProviderSpec",
APIVersion: "baremetal.cluster.k8s.io/v1alpha1",
},
// ObjectMeta - Only present because it's needed to form part of the runtime.RawExtension, not actually used by the provider.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Use provider’s SchemeGroupVersion for APIVersion

Avoid hardcoding; set APIVersion from the BareMetal provider scheme (openshift).

Apply:

 mapaProviderConfig := baremetalv1.BareMetalMachineProviderSpec{
   TypeMeta: metav1.TypeMeta{
     Kind:       "BareMetalMachineProviderSpec",
-    APIVersion: "baremetal.cluster.k8s.io/v1alpha1",
+    APIVersion: baremetalv1.SchemeGroupVersion.String(),
   },

Please confirm the group/version matches the version imported from github.com/openshift/cluster-api-provider-baremetal.


I'll verify the review comment by examining the codebase to confirm whether baremetalv1.SchemeGroupVersion exists and matches the hardcoded APIVersion.


🏁 Script executed:

#!/bin/bash
# 1. Check imports and the specific lines in metal3.go
echo "=== Checking imports and context in metal3.go ==="
head -50 pkg/conversion/capi2mapi/metal3.go | grep -E "^import|baremetalv1"
echo ""
echo "=== Lines 85-100 of metal3.go ==="
sed -n '85,100p' pkg/conversion/capi2mapi/metal3.go

Length of output: 900


🏁 Script executed:

#!/bin/bash
# 2. Search for SchemeGroupVersion in the entire codebase
echo "=== Searching for SchemeGroupVersion usage ==="
rg "SchemeGroupVersion" -n -B2 -A2

Length of output: 262526


🏁 Script executed:

#!/bin/bash
# 3. Check if baremetalv1 is imported and from where
echo "=== Searching for baremetalv1 package imports ==="
rg "baremetalv1" -n -C2 --type go | head -40

Length of output: 2928


Use baremetalv1.SchemeGroupVersion for APIVersion

The recommendation is valid. The hardcoded value "baremetal.cluster.k8s.io/v1alpha1" matches baremetalv1.SchemeGroupVersion defined in the provider's register.go. Replace the hardcoded string with baremetalv1.SchemeGroupVersion.String() to eliminate duplication and tie the version to the provider's scheme.

 mapaProviderConfig := baremetalv1.BareMetalMachineProviderSpec{
   TypeMeta: metav1.TypeMeta{
     Kind:       "BareMetalMachineProviderSpec",
-    APIVersion: "baremetal.cluster.k8s.io/v1alpha1",
+    APIVersion: baremetalv1.SchemeGroupVersion.String(),
   },
📝 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.

Suggested change
mapaProviderConfig := baremetalv1.BareMetalMachineProviderSpec{
TypeMeta: metav1.TypeMeta{
Kind: "BareMetalMachineProviderSpec",
APIVersion: "baremetal.cluster.k8s.io/v1alpha1",
},
// ObjectMeta - Only present because it's needed to form part of the runtime.RawExtension, not actually used by the provider.
mapaProviderConfig := baremetalv1.BareMetalMachineProviderSpec{
TypeMeta: metav1.TypeMeta{
Kind: "BareMetalMachineProviderSpec",
APIVersion: baremetalv1.SchemeGroupVersion.String(),
},
// ObjectMeta - Only present because it's needed to form part of the runtime.RawExtension, not actually used by the provider.
🤖 Prompt for AI Agents
In pkg/conversion/capi2mapi/metal3.go around lines 91 to 96, the APIVersion is
hardcoded as "baremetal.cluster.k8s.io/v1alpha1"; replace that hardcoded string
with baremetalv1.SchemeGroupVersion.String() so the APIVersion is derived from
the provider's scheme constant (ensure baremetalv1 is imported and used).

Comment on lines +160 to +164
capaMachine, ok := capaMachineObj.(*metal3v1.Metal3Machine)
if !ok {
panic(fmt.Errorf("%w: %T", errUnexpectedObjectTypeForMachine, capaMachineObj))
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t panic on unexpected infra type

Return an error instead of panicking in controller code.

Apply:

- if !ok {
-   panic(fmt.Errorf("%w: %T", errUnexpectedObjectTypeForMachine, capaMachineObj))
- }
+ if !ok {
+   return nil, nil, warnings, fmt.Errorf("%w: %T", errUnexpectedObjectTypeForMachine, capaMachineObj)
+ }
📝 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.

Suggested change
capaMachine, ok := capaMachineObj.(*metal3v1.Metal3Machine)
if !ok {
panic(fmt.Errorf("%w: %T", errUnexpectedObjectTypeForMachine, capaMachineObj))
}
capaMachine, ok := capaMachineObj.(*metal3v1.Metal3Machine)
if !ok {
return nil, nil, warnings, fmt.Errorf("%w: %T", errUnexpectedObjectTypeForMachine, capaMachineObj)
}
🤖 Prompt for AI Agents
In pkg/conversion/mapi2capi/metal3.go around lines 160 to 164, replace the panic
when capaMachineObj is not a *metal3v1.Metal3Machine with an error return:
remove the panic(fmt.Errorf(...)) and return a wrapped error (e.g.
fmt.Errorf("%w: %T", errUnexpectedObjectTypeForMachine, capaMachineObj)) from
the function; if the function currently does not return an error, update its
signature to return (resultType, error) or (error) as appropriate and propagate
the error to callers so they can handle it instead of allowing a panic.

Comment on lines +1 to +1352
// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
// https://github.com/sergi/go-diff
// See the included LICENSE file for license details.
//
// go-diff is a Go implementation of Google's Diff, Match, and Patch library
// Original library is Copyright (c) 2006 Google Inc.
// http://code.google.com/p/google-diff-match-patch/

package diffmatchpatch

import (
"bytes"
"errors"
"fmt"
"html"
"math"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
)

// Operation defines the operation of a diff item.
type Operation int8

//go:generate stringer -type=Operation -trimprefix=Diff

const (
// DiffDelete item represents a delete diff.
DiffDelete Operation = -1
// DiffInsert item represents an insert diff.
DiffInsert Operation = 1
// DiffEqual item represents an equal diff.
DiffEqual Operation = 0
//IndexSeparator is used to seperate the array indexes in an index string
IndexSeparator = ","
)

// Diff represents one diff operation
type Diff struct {
Type Operation
Text string
}

// splice removes amount elements from slice at index index, replacing them with elements.
func splice(slice []Diff, index int, amount int, elements ...Diff) []Diff {
if len(elements) == amount {
// Easy case: overwrite the relevant items.
copy(slice[index:], elements)
return slice
}
if len(elements) < amount {
// Fewer new items than old.
// Copy in the new items.
copy(slice[index:], elements)
// Shift the remaining items left.
copy(slice[index+len(elements):], slice[index+amount:])
// Calculate the new end of the slice.
end := len(slice) - amount + len(elements)
// Zero stranded elements at end so that they can be garbage collected.
tail := slice[end:]
for i := range tail {
tail[i] = Diff{}
}
return slice[:end]
}
// More new items than old.
// Make room in slice for new elements.
// There's probably an even more efficient way to do this,
// but this is simple and clear.
need := len(slice) - amount + len(elements)
for len(slice) < need {
slice = append(slice, Diff{})
}
// Shift slice elements right to make room for new elements.
copy(slice[index+len(elements):], slice[index+amount:])
// Copy in new elements.
copy(slice[index:], elements)
return slice
}

// DiffMain finds the differences between two texts.
// If an invalid UTF-8 sequence is encountered, it will be replaced by the Unicode replacement character.
func (dmp *DiffMatchPatch) DiffMain(text1, text2 string, checklines bool) []Diff {
return dmp.DiffMainRunes([]rune(text1), []rune(text2), checklines)
}

// DiffMainRunes finds the differences between two rune sequences.
// If an invalid UTF-8 sequence is encountered, it will be replaced by the Unicode replacement character.
func (dmp *DiffMatchPatch) DiffMainRunes(text1, text2 []rune, checklines bool) []Diff {
var deadline time.Time
if dmp.DiffTimeout > 0 {
deadline = time.Now().Add(dmp.DiffTimeout)
}
return dmp.diffMainRunes(text1, text2, checklines, deadline)
}

func (dmp *DiffMatchPatch) diffMainRunes(text1, text2 []rune, checklines bool, deadline time.Time) []Diff {
if runesEqual(text1, text2) {
var diffs []Diff
if len(text1) > 0 {
diffs = append(diffs, Diff{DiffEqual, string(text1)})
}
return diffs
}
// Trim off common prefix (speedup).
commonlength := commonPrefixLength(text1, text2)
commonprefix := text1[:commonlength]
text1 = text1[commonlength:]
text2 = text2[commonlength:]

// Trim off common suffix (speedup).
commonlength = commonSuffixLength(text1, text2)
commonsuffix := text1[len(text1)-commonlength:]
text1 = text1[:len(text1)-commonlength]
text2 = text2[:len(text2)-commonlength]

// Compute the diff on the middle block.
diffs := dmp.diffCompute(text1, text2, checklines, deadline)

// Restore the prefix and suffix.
if len(commonprefix) != 0 {
diffs = append([]Diff{{DiffEqual, string(commonprefix)}}, diffs...)
}
if len(commonsuffix) != 0 {
diffs = append(diffs, Diff{DiffEqual, string(commonsuffix)})
}

return dmp.DiffCleanupMerge(diffs)
}

// diffCompute finds the differences between two rune slices. Assumes that the texts do not have any common prefix or suffix.
func (dmp *DiffMatchPatch) diffCompute(text1, text2 []rune, checklines bool, deadline time.Time) []Diff {
diffs := []Diff{}
if len(text1) == 0 {
// Just add some text (speedup).
return append(diffs, Diff{DiffInsert, string(text2)})
} else if len(text2) == 0 {
// Just delete some text (speedup).
return append(diffs, Diff{DiffDelete, string(text1)})
}

var longtext, shorttext []rune
if len(text1) > len(text2) {
longtext = text1
shorttext = text2
} else {
longtext = text2
shorttext = text1
}

if i := runesIndex(longtext, shorttext); i != -1 {
op := DiffInsert
// Swap insertions for deletions if diff is reversed.
if len(text1) > len(text2) {
op = DiffDelete
}
// Shorter text is inside the longer text (speedup).
return []Diff{
Diff{op, string(longtext[:i])},
Diff{DiffEqual, string(shorttext)},
Diff{op, string(longtext[i+len(shorttext):])},
}
} else if len(shorttext) == 1 {
// Single character string.
// After the previous speedup, the character can't be an equality.
return []Diff{
{DiffDelete, string(text1)},
{DiffInsert, string(text2)},
}
// Check to see if the problem can be split in two.
} else if hm := dmp.diffHalfMatch(text1, text2); hm != nil {
// A half-match was found, sort out the return data.
text1A := hm[0]
text1B := hm[1]
text2A := hm[2]
text2B := hm[3]
midCommon := hm[4]
// Send both pairs off for separate processing.
diffsA := dmp.diffMainRunes(text1A, text2A, checklines, deadline)
diffsB := dmp.diffMainRunes(text1B, text2B, checklines, deadline)
// Merge the results.
diffs := diffsA
diffs = append(diffs, Diff{DiffEqual, string(midCommon)})
diffs = append(diffs, diffsB...)
return diffs
} else if checklines && len(text1) > 100 && len(text2) > 100 {
return dmp.diffLineMode(text1, text2, deadline)
}
return dmp.diffBisect(text1, text2, deadline)
}

// diffLineMode does a quick line-level diff on both []runes, then rediff the parts for greater accuracy. This speedup can produce non-minimal diffs.
func (dmp *DiffMatchPatch) diffLineMode(text1, text2 []rune, deadline time.Time) []Diff {
// Scan the text on a line-by-line basis first.
text1, text2, linearray := dmp.DiffLinesToRunes(string(text1), string(text2))

diffs := dmp.diffMainRunes(text1, text2, false, deadline)

// Convert the diff back to original text.
diffs = dmp.DiffCharsToLines(diffs, linearray)
// Eliminate freak matches (e.g. blank lines)
diffs = dmp.DiffCleanupSemantic(diffs)

// Rediff any replacement blocks, this time character-by-character.
// Add a dummy entry at the end.
diffs = append(diffs, Diff{DiffEqual, ""})

pointer := 0
countDelete := 0
countInsert := 0

// NOTE: Rune slices are slower than using strings in this case.
textDelete := ""
textInsert := ""

for pointer < len(diffs) {
switch diffs[pointer].Type {
case DiffInsert:
countInsert++
textInsert += diffs[pointer].Text
case DiffDelete:
countDelete++
textDelete += diffs[pointer].Text
case DiffEqual:
// Upon reaching an equality, check for prior redundancies.
if countDelete >= 1 && countInsert >= 1 {
// Delete the offending records and add the merged ones.
diffs = splice(diffs, pointer-countDelete-countInsert,
countDelete+countInsert)

pointer = pointer - countDelete - countInsert
a := dmp.diffMainRunes([]rune(textDelete), []rune(textInsert), false, deadline)
for j := len(a) - 1; j >= 0; j-- {
diffs = splice(diffs, pointer, 0, a[j])
}
pointer = pointer + len(a)
}

countInsert = 0
countDelete = 0
textDelete = ""
textInsert = ""
}
pointer++
}

return diffs[:len(diffs)-1] // Remove the dummy entry at the end.
}

// DiffBisect finds the 'middle snake' of a diff, split the problem in two and return the recursively constructed diff.
// If an invalid UTF-8 sequence is encountered, it will be replaced by the Unicode replacement character.
// See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
func (dmp *DiffMatchPatch) DiffBisect(text1, text2 string, deadline time.Time) []Diff {
// Unused in this code, but retained for interface compatibility.
return dmp.diffBisect([]rune(text1), []rune(text2), deadline)
}

// diffBisect finds the 'middle snake' of a diff, splits the problem in two and returns the recursively constructed diff.
// See Myers's 1986 paper: An O(ND) Difference Algorithm and Its Variations.
func (dmp *DiffMatchPatch) diffBisect(runes1, runes2 []rune, deadline time.Time) []Diff {
// Cache the text lengths to prevent multiple calls.
runes1Len, runes2Len := len(runes1), len(runes2)

maxD := (runes1Len + runes2Len + 1) / 2
vOffset := maxD
vLength := 2 * maxD

v1 := make([]int, vLength)
v2 := make([]int, vLength)
for i := range v1 {
v1[i] = -1
v2[i] = -1
}
v1[vOffset+1] = 0
v2[vOffset+1] = 0

delta := runes1Len - runes2Len
// If the total number of characters is odd, then the front path will collide with the reverse path.
front := (delta%2 != 0)
// Offsets for start and end of k loop. Prevents mapping of space beyond the grid.
k1start := 0
k1end := 0
k2start := 0
k2end := 0
for d := 0; d < maxD; d++ {
// Bail out if deadline is reached.
if !deadline.IsZero() && d%16 == 0 && time.Now().After(deadline) {
break
}

// Walk the front path one step.
for k1 := -d + k1start; k1 <= d-k1end; k1 += 2 {
k1Offset := vOffset + k1
var x1 int

if k1 == -d || (k1 != d && v1[k1Offset-1] < v1[k1Offset+1]) {
x1 = v1[k1Offset+1]
} else {
x1 = v1[k1Offset-1] + 1
}

y1 := x1 - k1
for x1 < runes1Len && y1 < runes2Len {
if runes1[x1] != runes2[y1] {
break
}
x1++
y1++
}
v1[k1Offset] = x1
if x1 > runes1Len {
// Ran off the right of the graph.
k1end += 2
} else if y1 > runes2Len {
// Ran off the bottom of the graph.
k1start += 2
} else if front {
k2Offset := vOffset + delta - k1
if k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] != -1 {
// Mirror x2 onto top-left coordinate system.
x2 := runes1Len - v2[k2Offset]
if x1 >= x2 {
// Overlap detected.
return dmp.diffBisectSplit(runes1, runes2, x1, y1, deadline)
}
}
}
}
// Walk the reverse path one step.
for k2 := -d + k2start; k2 <= d-k2end; k2 += 2 {
k2Offset := vOffset + k2
var x2 int
if k2 == -d || (k2 != d && v2[k2Offset-1] < v2[k2Offset+1]) {
x2 = v2[k2Offset+1]
} else {
x2 = v2[k2Offset-1] + 1
}
var y2 = x2 - k2
for x2 < runes1Len && y2 < runes2Len {
if runes1[runes1Len-x2-1] != runes2[runes2Len-y2-1] {
break
}
x2++
y2++
}
v2[k2Offset] = x2
if x2 > runes1Len {
// Ran off the left of the graph.
k2end += 2
} else if y2 > runes2Len {
// Ran off the top of the graph.
k2start += 2
} else if !front {
k1Offset := vOffset + delta - k2
if k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] != -1 {
x1 := v1[k1Offset]
y1 := vOffset + x1 - k1Offset
// Mirror x2 onto top-left coordinate system.
x2 = runes1Len - x2
if x1 >= x2 {
// Overlap detected.
return dmp.diffBisectSplit(runes1, runes2, x1, y1, deadline)
}
}
}
}
}
// Diff took too long and hit the deadline or number of diffs equals number of characters, no commonality at all.
return []Diff{
{DiffDelete, string(runes1)},
{DiffInsert, string(runes2)},
}
}

func (dmp *DiffMatchPatch) diffBisectSplit(runes1, runes2 []rune, x, y int,
deadline time.Time) []Diff {
runes1a := runes1[:x]
runes2a := runes2[:y]
runes1b := runes1[x:]
runes2b := runes2[y:]

// Compute both diffs serially.
diffs := dmp.diffMainRunes(runes1a, runes2a, false, deadline)
diffsb := dmp.diffMainRunes(runes1b, runes2b, false, deadline)

return append(diffs, diffsb...)
}

// DiffLinesToChars splits two texts into a list of strings, and educes the texts to a string of hashes where each Unicode character represents one line.
// It's slightly faster to call DiffLinesToRunes first, followed by DiffMainRunes.
func (dmp *DiffMatchPatch) DiffLinesToChars(text1, text2 string) (string, string, []string) {
chars1, chars2, lineArray := dmp.diffLinesToStrings(text1, text2)
return chars1, chars2, lineArray
}

// DiffLinesToRunes splits two texts into a list of runes.
func (dmp *DiffMatchPatch) DiffLinesToRunes(text1, text2 string) ([]rune, []rune, []string) {
chars1, chars2, lineArray := dmp.diffLinesToStrings(text1, text2)
return []rune(chars1), []rune(chars2), lineArray
}

// DiffCharsToLines rehydrates the text in a diff from a string of line hashes to real lines of text.
func (dmp *DiffMatchPatch) DiffCharsToLines(diffs []Diff, lineArray []string) []Diff {
hydrated := make([]Diff, 0, len(diffs))
for _, aDiff := range diffs {
chars := strings.Split(aDiff.Text, IndexSeparator)
text := make([]string, len(chars))

for i, r := range chars {
i1, err := strconv.Atoi(r)
if err == nil {
text[i] = lineArray[i1]
}
}

aDiff.Text = strings.Join(text, "")
hydrated = append(hydrated, aDiff)
}
return hydrated
}

// DiffCommonPrefix determines the common prefix length of two strings.
func (dmp *DiffMatchPatch) DiffCommonPrefix(text1, text2 string) int {
// Unused in this code, but retained for interface compatibility.
return commonPrefixLength([]rune(text1), []rune(text2))
}

// DiffCommonSuffix determines the common suffix length of two strings.
func (dmp *DiffMatchPatch) DiffCommonSuffix(text1, text2 string) int {
// Unused in this code, but retained for interface compatibility.
return commonSuffixLength([]rune(text1), []rune(text2))
}

// commonPrefixLength returns the length of the common prefix of two rune slices.
func commonPrefixLength(text1, text2 []rune) int {
// Linear search. See comment in commonSuffixLength.
n := 0
for ; n < len(text1) && n < len(text2); n++ {
if text1[n] != text2[n] {
return n
}
}
return n
}

// commonSuffixLength returns the length of the common suffix of two rune slices.
func commonSuffixLength(text1, text2 []rune) int {
// Use linear search rather than the binary search discussed at https://neil.fraser.name/news/2007/10/09/.
// See discussion at https://github.com/sergi/go-diff/issues/54.
i1 := len(text1)
i2 := len(text2)
for n := 0; ; n++ {
i1--
i2--
if i1 < 0 || i2 < 0 || text1[i1] != text2[i2] {
return n
}
}
}

// DiffCommonOverlap determines if the suffix of one string is the prefix of another.
func (dmp *DiffMatchPatch) DiffCommonOverlap(text1 string, text2 string) int {
// Cache the text lengths to prevent multiple calls.
text1Length := len(text1)
text2Length := len(text2)
// Eliminate the null case.
if text1Length == 0 || text2Length == 0 {
return 0
}
// Truncate the longer string.
if text1Length > text2Length {
text1 = text1[text1Length-text2Length:]
} else if text1Length < text2Length {
text2 = text2[0:text1Length]
}
textLength := int(math.Min(float64(text1Length), float64(text2Length)))
// Quick check for the worst case.
if text1 == text2 {
return textLength
}

// Start by looking for a single character match and increase length until no match is found. Performance analysis: http://neil.fraser.name/news/2010/11/04/
best := 0
length := 1
for {
pattern := text1[textLength-length:]
found := strings.Index(text2, pattern)
if found == -1 {
break
}
length += found
if found == 0 || text1[textLength-length:] == text2[0:length] {
best = length
length++
}
}

return best
}

// DiffHalfMatch checks whether the two texts share a substring which is at least half the length of the longer text. This speedup can produce non-minimal diffs.
func (dmp *DiffMatchPatch) DiffHalfMatch(text1, text2 string) []string {
// Unused in this code, but retained for interface compatibility.
runeSlices := dmp.diffHalfMatch([]rune(text1), []rune(text2))
if runeSlices == nil {
return nil
}

result := make([]string, len(runeSlices))
for i, r := range runeSlices {
result[i] = string(r)
}
return result
}

func (dmp *DiffMatchPatch) diffHalfMatch(text1, text2 []rune) [][]rune {
if dmp.DiffTimeout <= 0 {
// Don't risk returning a non-optimal diff if we have unlimited time.
return nil
}

var longtext, shorttext []rune
if len(text1) > len(text2) {
longtext = text1
shorttext = text2
} else {
longtext = text2
shorttext = text1
}

if len(longtext) < 4 || len(shorttext)*2 < len(longtext) {
return nil // Pointless.
}

// First check if the second quarter is the seed for a half-match.
hm1 := dmp.diffHalfMatchI(longtext, shorttext, int(float64(len(longtext)+3)/4))

// Check again based on the third quarter.
hm2 := dmp.diffHalfMatchI(longtext, shorttext, int(float64(len(longtext)+1)/2))

hm := [][]rune{}
if hm1 == nil && hm2 == nil {
return nil
} else if hm2 == nil {
hm = hm1
} else if hm1 == nil {
hm = hm2
} else {
// Both matched. Select the longest.
if len(hm1[4]) > len(hm2[4]) {
hm = hm1
} else {
hm = hm2
}
}

// A half-match was found, sort out the return data.
if len(text1) > len(text2) {
return hm
}

return [][]rune{hm[2], hm[3], hm[0], hm[1], hm[4]}
}

// diffHalfMatchI checks if a substring of shorttext exist within longtext such that the substring is at least half the length of longtext?
// Returns a slice containing the prefix of longtext, the suffix of longtext, the prefix of shorttext, the suffix of shorttext and the common middle, or null if there was no match.
func (dmp *DiffMatchPatch) diffHalfMatchI(l, s []rune, i int) [][]rune {
var bestCommonA []rune
var bestCommonB []rune
var bestCommonLen int
var bestLongtextA []rune
var bestLongtextB []rune
var bestShorttextA []rune
var bestShorttextB []rune

// Start with a 1/4 length substring at position i as a seed.
seed := l[i : i+len(l)/4]

for j := runesIndexOf(s, seed, 0); j != -1; j = runesIndexOf(s, seed, j+1) {
prefixLength := commonPrefixLength(l[i:], s[j:])
suffixLength := commonSuffixLength(l[:i], s[:j])

if bestCommonLen < suffixLength+prefixLength {
bestCommonA = s[j-suffixLength : j]
bestCommonB = s[j : j+prefixLength]
bestCommonLen = len(bestCommonA) + len(bestCommonB)
bestLongtextA = l[:i-suffixLength]
bestLongtextB = l[i+prefixLength:]
bestShorttextA = s[:j-suffixLength]
bestShorttextB = s[j+prefixLength:]
}
}

if bestCommonLen*2 < len(l) {
return nil
}

return [][]rune{
bestLongtextA,
bestLongtextB,
bestShorttextA,
bestShorttextB,
append(bestCommonA, bestCommonB...),
}
}

// DiffCleanupSemantic reduces the number of edits by eliminating semantically trivial equalities.
func (dmp *DiffMatchPatch) DiffCleanupSemantic(diffs []Diff) []Diff {
changes := false
// Stack of indices where equalities are found.
equalities := make([]int, 0, len(diffs))

var lastequality string
// Always equal to diffs[equalities[equalitiesLength - 1]][1]
var pointer int // Index of current position.
// Number of characters that changed prior to the equality.
var lengthInsertions1, lengthDeletions1 int
// Number of characters that changed after the equality.
var lengthInsertions2, lengthDeletions2 int

for pointer < len(diffs) {
if diffs[pointer].Type == DiffEqual {
// Equality found.
equalities = append(equalities, pointer)
lengthInsertions1 = lengthInsertions2
lengthDeletions1 = lengthDeletions2
lengthInsertions2 = 0
lengthDeletions2 = 0
lastequality = diffs[pointer].Text
} else {
// An insertion or deletion.

if diffs[pointer].Type == DiffInsert {
lengthInsertions2 += utf8.RuneCountInString(diffs[pointer].Text)
} else {
lengthDeletions2 += utf8.RuneCountInString(diffs[pointer].Text)
}
// Eliminate an equality that is smaller or equal to the edits on both sides of it.
difference1 := int(math.Max(float64(lengthInsertions1), float64(lengthDeletions1)))
difference2 := int(math.Max(float64(lengthInsertions2), float64(lengthDeletions2)))
if utf8.RuneCountInString(lastequality) > 0 &&
(utf8.RuneCountInString(lastequality) <= difference1) &&
(utf8.RuneCountInString(lastequality) <= difference2) {
// Duplicate record.
insPoint := equalities[len(equalities)-1]
diffs = splice(diffs, insPoint, 0, Diff{DiffDelete, lastequality})

// Change second copy to insert.
diffs[insPoint+1].Type = DiffInsert
// Throw away the equality we just deleted.
equalities = equalities[:len(equalities)-1]

if len(equalities) > 0 {
equalities = equalities[:len(equalities)-1]
}
pointer = -1
if len(equalities) > 0 {
pointer = equalities[len(equalities)-1]
}

lengthInsertions1 = 0 // Reset the counters.
lengthDeletions1 = 0
lengthInsertions2 = 0
lengthDeletions2 = 0
lastequality = ""
changes = true
}
}
pointer++
}

// Normalize the diff.
if changes {
diffs = dmp.DiffCleanupMerge(diffs)
}
diffs = dmp.DiffCleanupSemanticLossless(diffs)
// Find any overlaps between deletions and insertions.
// e.g: <del>abcxxx</del><ins>xxxdef</ins>
// -> <del>abc</del>xxx<ins>def</ins>
// e.g: <del>xxxabc</del><ins>defxxx</ins>
// -> <ins>def</ins>xxx<del>abc</del>
// Only extract an overlap if it is as big as the edit ahead or behind it.
pointer = 1
for pointer < len(diffs) {
if diffs[pointer-1].Type == DiffDelete &&
diffs[pointer].Type == DiffInsert {
deletion := diffs[pointer-1].Text
insertion := diffs[pointer].Text
overlapLength1 := dmp.DiffCommonOverlap(deletion, insertion)
overlapLength2 := dmp.DiffCommonOverlap(insertion, deletion)
if overlapLength1 >= overlapLength2 {
if float64(overlapLength1) >= float64(utf8.RuneCountInString(deletion))/2 ||
float64(overlapLength1) >= float64(utf8.RuneCountInString(insertion))/2 {

// Overlap found. Insert an equality and trim the surrounding edits.
diffs = splice(diffs, pointer, 0, Diff{DiffEqual, insertion[:overlapLength1]})
diffs[pointer-1].Text =
deletion[0 : len(deletion)-overlapLength1]
diffs[pointer+1].Text = insertion[overlapLength1:]
pointer++
}
} else {
if float64(overlapLength2) >= float64(utf8.RuneCountInString(deletion))/2 ||
float64(overlapLength2) >= float64(utf8.RuneCountInString(insertion))/2 {
// Reverse overlap found. Insert an equality and swap and trim the surrounding edits.
overlap := Diff{DiffEqual, deletion[:overlapLength2]}
diffs = splice(diffs, pointer, 0, overlap)
diffs[pointer-1].Type = DiffInsert
diffs[pointer-1].Text = insertion[0 : len(insertion)-overlapLength2]
diffs[pointer+1].Type = DiffDelete
diffs[pointer+1].Text = deletion[overlapLength2:]
pointer++
}
}
pointer++
}
pointer++
}

return diffs
}

// Define some regex patterns for matching boundaries.
var (
nonAlphaNumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]`)
whitespaceRegex = regexp.MustCompile(`\s`)
linebreakRegex = regexp.MustCompile(`[\r\n]`)
blanklineEndRegex = regexp.MustCompile(`\n\r?\n$`)
blanklineStartRegex = regexp.MustCompile(`^\r?\n\r?\n`)
)

// diffCleanupSemanticScore computes a score representing whether the internal boundary falls on logical boundaries.
// Scores range from 6 (best) to 0 (worst). Closure, but does not reference any external variables.
func diffCleanupSemanticScore(one, two string) int {
if len(one) == 0 || len(two) == 0 {
// Edges are the best.
return 6
}

// Each port of this function behaves slightly differently due to subtle differences in each language's definition of things like 'whitespace'. Since this function's purpose is largely cosmetic, the choice has been made to use each language's native features rather than force total conformity.
rune1, _ := utf8.DecodeLastRuneInString(one)
rune2, _ := utf8.DecodeRuneInString(two)
char1 := string(rune1)
char2 := string(rune2)

nonAlphaNumeric1 := nonAlphaNumericRegex.MatchString(char1)
nonAlphaNumeric2 := nonAlphaNumericRegex.MatchString(char2)
whitespace1 := nonAlphaNumeric1 && whitespaceRegex.MatchString(char1)
whitespace2 := nonAlphaNumeric2 && whitespaceRegex.MatchString(char2)
lineBreak1 := whitespace1 && linebreakRegex.MatchString(char1)
lineBreak2 := whitespace2 && linebreakRegex.MatchString(char2)
blankLine1 := lineBreak1 && blanklineEndRegex.MatchString(one)
blankLine2 := lineBreak2 && blanklineEndRegex.MatchString(two)

if blankLine1 || blankLine2 {
// Five points for blank lines.
return 5
} else if lineBreak1 || lineBreak2 {
// Four points for line breaks.
return 4
} else if nonAlphaNumeric1 && !whitespace1 && whitespace2 {
// Three points for end of sentences.
return 3
} else if whitespace1 || whitespace2 {
// Two points for whitespace.
return 2
} else if nonAlphaNumeric1 || nonAlphaNumeric2 {
// One point for non-alphanumeric.
return 1
}
return 0
}

// DiffCleanupSemanticLossless looks for single edits surrounded on both sides by equalities which can be shifted sideways to align the edit to a word boundary.
// E.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
func (dmp *DiffMatchPatch) DiffCleanupSemanticLossless(diffs []Diff) []Diff {
pointer := 1

// Intentionally ignore the first and last element (don't need checking).
for pointer < len(diffs)-1 {
if diffs[pointer-1].Type == DiffEqual &&
diffs[pointer+1].Type == DiffEqual {

// This is a single edit surrounded by equalities.
equality1 := diffs[pointer-1].Text
edit := diffs[pointer].Text
equality2 := diffs[pointer+1].Text

// First, shift the edit as far left as possible.
commonOffset := dmp.DiffCommonSuffix(equality1, edit)
if commonOffset > 0 {
commonString := edit[len(edit)-commonOffset:]
equality1 = equality1[0 : len(equality1)-commonOffset]
edit = commonString + edit[:len(edit)-commonOffset]
equality2 = commonString + equality2
}

// Second, step character by character right, looking for the best fit.
bestEquality1 := equality1
bestEdit := edit
bestEquality2 := equality2
bestScore := diffCleanupSemanticScore(equality1, edit) +
diffCleanupSemanticScore(edit, equality2)

for len(edit) != 0 && len(equality2) != 0 {
_, sz := utf8.DecodeRuneInString(edit)
if len(equality2) < sz || edit[:sz] != equality2[:sz] {
break
}
equality1 += edit[:sz]
edit = edit[sz:] + equality2[:sz]
equality2 = equality2[sz:]
score := diffCleanupSemanticScore(equality1, edit) +
diffCleanupSemanticScore(edit, equality2)
// The >= encourages trailing rather than leading whitespace on edits.
if score >= bestScore {
bestScore = score
bestEquality1 = equality1
bestEdit = edit
bestEquality2 = equality2
}
}

if diffs[pointer-1].Text != bestEquality1 {
// We have an improvement, save it back to the diff.
if len(bestEquality1) != 0 {
diffs[pointer-1].Text = bestEquality1
} else {
diffs = splice(diffs, pointer-1, 1)
pointer--
}

diffs[pointer].Text = bestEdit
if len(bestEquality2) != 0 {
diffs[pointer+1].Text = bestEquality2
} else {
diffs = append(diffs[:pointer+1], diffs[pointer+2:]...)
pointer--
}
}
}
pointer++
}

return diffs
}

// DiffCleanupEfficiency reduces the number of edits by eliminating operationally trivial equalities.
func (dmp *DiffMatchPatch) DiffCleanupEfficiency(diffs []Diff) []Diff {
changes := false
// Stack of indices where equalities are found.
type equality struct {
data int
next *equality
}
var equalities *equality
// Always equal to equalities[equalitiesLength-1][1]
lastequality := ""
pointer := 0 // Index of current position.
// Is there an insertion operation before the last equality.
preIns := false
// Is there a deletion operation before the last equality.
preDel := false
// Is there an insertion operation after the last equality.
postIns := false
// Is there a deletion operation after the last equality.
postDel := false
for pointer < len(diffs) {
if diffs[pointer].Type == DiffEqual { // Equality found.
if len(diffs[pointer].Text) < dmp.DiffEditCost &&
(postIns || postDel) {
// Candidate found.
equalities = &equality{
data: pointer,
next: equalities,
}
preIns = postIns
preDel = postDel
lastequality = diffs[pointer].Text
} else {
// Not a candidate, and can never become one.
equalities = nil
lastequality = ""
}
postIns = false
postDel = false
} else { // An insertion or deletion.
if diffs[pointer].Type == DiffDelete {
postDel = true
} else {
postIns = true
}

// Five types to be split:
// <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
// <ins>A</ins>X<ins>C</ins><del>D</del>
// <ins>A</ins><del>B</del>X<ins>C</ins>
// <ins>A</del>X<ins>C</ins><del>D</del>
// <ins>A</ins><del>B</del>X<del>C</del>
var sumPres int
if preIns {
sumPres++
}
if preDel {
sumPres++
}
if postIns {
sumPres++
}
if postDel {
sumPres++
}
if len(lastequality) > 0 &&
((preIns && preDel && postIns && postDel) ||
((len(lastequality) < dmp.DiffEditCost/2) && sumPres == 3)) {

insPoint := equalities.data

// Duplicate record.
diffs = splice(diffs, insPoint, 0, Diff{DiffDelete, lastequality})

// Change second copy to insert.
diffs[insPoint+1].Type = DiffInsert
// Throw away the equality we just deleted.
equalities = equalities.next
lastequality = ""

if preIns && preDel {
// No changes made which could affect previous entry, keep going.
postIns = true
postDel = true
equalities = nil
} else {
if equalities != nil {
equalities = equalities.next
}
if equalities != nil {
pointer = equalities.data
} else {
pointer = -1
}
postIns = false
postDel = false
}
changes = true
}
}
pointer++
}

if changes {
diffs = dmp.DiffCleanupMerge(diffs)
}

return diffs
}

// DiffCleanupMerge reorders and merges like edit sections. Merge equalities.
// Any edit section can move as long as it doesn't cross an equality.
func (dmp *DiffMatchPatch) DiffCleanupMerge(diffs []Diff) []Diff {
// Add a dummy entry at the end.
diffs = append(diffs, Diff{DiffEqual, ""})
pointer := 0
countDelete := 0
countInsert := 0
commonlength := 0
textDelete := []rune(nil)
textInsert := []rune(nil)

for pointer < len(diffs) {
switch diffs[pointer].Type {
case DiffInsert:
countInsert++
textInsert = append(textInsert, []rune(diffs[pointer].Text)...)
pointer++
break
case DiffDelete:
countDelete++
textDelete = append(textDelete, []rune(diffs[pointer].Text)...)
pointer++
break
case DiffEqual:
// Upon reaching an equality, check for prior redundancies.
if countDelete+countInsert > 1 {
if countDelete != 0 && countInsert != 0 {
// Factor out any common prefixies.
commonlength = commonPrefixLength(textInsert, textDelete)
if commonlength != 0 {
x := pointer - countDelete - countInsert
if x > 0 && diffs[x-1].Type == DiffEqual {
diffs[x-1].Text += string(textInsert[:commonlength])
} else {
diffs = append([]Diff{{DiffEqual, string(textInsert[:commonlength])}}, diffs...)
pointer++
}
textInsert = textInsert[commonlength:]
textDelete = textDelete[commonlength:]
}
// Factor out any common suffixies.
commonlength = commonSuffixLength(textInsert, textDelete)
if commonlength != 0 {
insertIndex := len(textInsert) - commonlength
deleteIndex := len(textDelete) - commonlength
diffs[pointer].Text = string(textInsert[insertIndex:]) + diffs[pointer].Text
textInsert = textInsert[:insertIndex]
textDelete = textDelete[:deleteIndex]
}
}
// Delete the offending records and add the merged ones.
if countDelete == 0 {
diffs = splice(diffs, pointer-countInsert,
countDelete+countInsert,
Diff{DiffInsert, string(textInsert)})
} else if countInsert == 0 {
diffs = splice(diffs, pointer-countDelete,
countDelete+countInsert,
Diff{DiffDelete, string(textDelete)})
} else {
diffs = splice(diffs, pointer-countDelete-countInsert,
countDelete+countInsert,
Diff{DiffDelete, string(textDelete)},
Diff{DiffInsert, string(textInsert)})
}

pointer = pointer - countDelete - countInsert + 1
if countDelete != 0 {
pointer++
}
if countInsert != 0 {
pointer++
}
} else if pointer != 0 && diffs[pointer-1].Type == DiffEqual {
// Merge this equality with the previous one.
diffs[pointer-1].Text += diffs[pointer].Text
diffs = append(diffs[:pointer], diffs[pointer+1:]...)
} else {
pointer++
}
countInsert = 0
countDelete = 0
textDelete = nil
textInsert = nil
break
}
}

if len(diffs[len(diffs)-1].Text) == 0 {
diffs = diffs[0 : len(diffs)-1] // Remove the dummy entry at the end.
}

// Second pass: look for single edits surrounded on both sides by equalities which can be shifted sideways to eliminate an equality. E.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
changes := false
pointer = 1
// Intentionally ignore the first and last element (don't need checking).
for pointer < (len(diffs) - 1) {
if diffs[pointer-1].Type == DiffEqual &&
diffs[pointer+1].Type == DiffEqual {
// This is a single edit surrounded by equalities.
if strings.HasSuffix(diffs[pointer].Text, diffs[pointer-1].Text) {
// Shift the edit over the previous equality.
diffs[pointer].Text = diffs[pointer-1].Text +
diffs[pointer].Text[:len(diffs[pointer].Text)-len(diffs[pointer-1].Text)]
diffs[pointer+1].Text = diffs[pointer-1].Text + diffs[pointer+1].Text
diffs = splice(diffs, pointer-1, 1)
changes = true
} else if strings.HasPrefix(diffs[pointer].Text, diffs[pointer+1].Text) {
// Shift the edit over the next equality.
diffs[pointer-1].Text += diffs[pointer+1].Text
diffs[pointer].Text =
diffs[pointer].Text[len(diffs[pointer+1].Text):] + diffs[pointer+1].Text
diffs = splice(diffs, pointer+1, 1)
changes = true
}
}
pointer++
}

// If shifts were made, the diff needs reordering and another shift sweep.
if changes {
diffs = dmp.DiffCleanupMerge(diffs)
}

return diffs
}

// DiffXIndex returns the equivalent location in s2.
func (dmp *DiffMatchPatch) DiffXIndex(diffs []Diff, loc int) int {
chars1 := 0
chars2 := 0
lastChars1 := 0
lastChars2 := 0
lastDiff := Diff{}
for i := 0; i < len(diffs); i++ {
aDiff := diffs[i]
if aDiff.Type != DiffInsert {
// Equality or deletion.
chars1 += len(aDiff.Text)
}
if aDiff.Type != DiffDelete {
// Equality or insertion.
chars2 += len(aDiff.Text)
}
if chars1 > loc {
// Overshot the location.
lastDiff = aDiff
break
}
lastChars1 = chars1
lastChars2 = chars2
}
if lastDiff.Type == DiffDelete {
// The location was deleted.
return lastChars2
}
// Add the remaining character length.
return lastChars2 + (loc - lastChars1)
}

// DiffPrettyHtml converts a []Diff into a pretty HTML report.
// It is intended as an example from which to write one's own display functions.
func (dmp *DiffMatchPatch) DiffPrettyHtml(diffs []Diff) string {
var buff bytes.Buffer
for _, diff := range diffs {
text := strings.Replace(html.EscapeString(diff.Text), "\n", "&para;<br>", -1)
switch diff.Type {
case DiffInsert:
_, _ = buff.WriteString("<ins style=\"background:#e6ffe6;\">")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</ins>")
case DiffDelete:
_, _ = buff.WriteString("<del style=\"background:#ffe6e6;\">")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</del>")
case DiffEqual:
_, _ = buff.WriteString("<span>")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("</span>")
}
}
return buff.String()
}

// DiffPrettyText converts a []Diff into a colored text report.
func (dmp *DiffMatchPatch) DiffPrettyText(diffs []Diff) string {
var buff bytes.Buffer
for _, diff := range diffs {
text := diff.Text

switch diff.Type {
case DiffInsert:
_, _ = buff.WriteString("\x1b[32m")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("\x1b[0m")
case DiffDelete:
_, _ = buff.WriteString("\x1b[31m")
_, _ = buff.WriteString(text)
_, _ = buff.WriteString("\x1b[0m")
case DiffEqual:
_, _ = buff.WriteString(text)
}
}

return buff.String()
}

// DiffText1 computes and returns the source text (all equalities and deletions).
func (dmp *DiffMatchPatch) DiffText1(diffs []Diff) string {
//StringBuilder text = new StringBuilder()
var text bytes.Buffer

for _, aDiff := range diffs {
if aDiff.Type != DiffInsert {
_, _ = text.WriteString(aDiff.Text)
}
}
return text.String()
}

// DiffText2 computes and returns the destination text (all equalities and insertions).
func (dmp *DiffMatchPatch) DiffText2(diffs []Diff) string {
var text bytes.Buffer

for _, aDiff := range diffs {
if aDiff.Type != DiffDelete {
_, _ = text.WriteString(aDiff.Text)
}
}
return text.String()
}

// DiffLevenshtein computes the Levenshtein distance that is the number of inserted, deleted or substituted characters.
func (dmp *DiffMatchPatch) DiffLevenshtein(diffs []Diff) int {
levenshtein := 0
insertions := 0
deletions := 0

for _, aDiff := range diffs {
switch aDiff.Type {
case DiffInsert:
insertions += utf8.RuneCountInString(aDiff.Text)
case DiffDelete:
deletions += utf8.RuneCountInString(aDiff.Text)
case DiffEqual:
// A deletion and an insertion is one substitution.
levenshtein += max(insertions, deletions)
insertions = 0
deletions = 0
}
}

levenshtein += max(insertions, deletions)
return levenshtein
}

// DiffToDelta crushes the diff into an encoded string which describes the operations required to transform text1 into text2.
// E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. Operations are tab-separated. Inserted text is escaped using %xx notation.
func (dmp *DiffMatchPatch) DiffToDelta(diffs []Diff) string {
var text bytes.Buffer
for _, aDiff := range diffs {
switch aDiff.Type {
case DiffInsert:
_, _ = text.WriteString("+")
_, _ = text.WriteString(strings.Replace(url.QueryEscape(aDiff.Text), "+", " ", -1))
_, _ = text.WriteString("\t")
break
case DiffDelete:
_, _ = text.WriteString("-")
_, _ = text.WriteString(strconv.Itoa(utf8.RuneCountInString(aDiff.Text)))
_, _ = text.WriteString("\t")
break
case DiffEqual:
_, _ = text.WriteString("=")
_, _ = text.WriteString(strconv.Itoa(utf8.RuneCountInString(aDiff.Text)))
_, _ = text.WriteString("\t")
break
}
}
delta := text.String()
if len(delta) != 0 {
// Strip off trailing tab character.
delta = delta[0 : utf8.RuneCountInString(delta)-1]
delta = unescaper.Replace(delta)
}
return delta
}

// DiffFromDelta given the original text1, and an encoded string which describes the operations required to transform text1 into text2, comAdde the full diff.
func (dmp *DiffMatchPatch) DiffFromDelta(text1 string, delta string) (diffs []Diff, err error) {
i := 0
runes := []rune(text1)

for _, token := range strings.Split(delta, "\t") {
if len(token) == 0 {
// Blank tokens are ok (from a trailing \t).
continue
}

// Each token begins with a one character parameter which specifies the operation of this token (delete, insert, equality).
param := token[1:]

switch op := token[0]; op {
case '+':
// Decode would Diff all "+" to " "
param = strings.Replace(param, "+", "%2b", -1)
param, err = url.QueryUnescape(param)
if err != nil {
return nil, err
}
if !utf8.ValidString(param) {
return nil, fmt.Errorf("invalid UTF-8 token: %q", param)
}

diffs = append(diffs, Diff{DiffInsert, param})
case '=', '-':
n, err := strconv.ParseInt(param, 10, 0)
if err != nil {
return nil, err
} else if n < 0 {
return nil, errors.New("Negative number in DiffFromDelta: " + param)
}

i += int(n)
// Break out if we are out of bounds, go1.6 can't handle this very well
if i > len(runes) {
break
}
// Remember that string slicing is by byte - we want by rune here.
text := string(runes[i-int(n) : i])

if op == '=' {
diffs = append(diffs, Diff{DiffEqual, text})
} else {
diffs = append(diffs, Diff{DiffDelete, text})
}
default:
// Anything else is an error.
return nil, errors.New("Invalid diff operation in DiffFromDelta: " + string(token[0]))
}
}

if i != len(runes) {
return nil, fmt.Errorf("Delta length (%v) is different from source text length (%v)", i, len(text1))
}

return diffs, nil
}

// diffLinesToStrings splits two texts into a list of strings. Each string represents one line.
func (dmp *DiffMatchPatch) diffLinesToStrings(text1, text2 string) (string, string, []string) {
// '\x00' is a valid character, but various debuggers don't like it. So we'll insert a junk entry to avoid generating a null character.
lineArray := []string{""} // e.g. lineArray[4] == 'Hello\n'

lineHash := make(map[string]int)
//Each string has the index of lineArray which it points to
strIndexArray1 := dmp.diffLinesToStringsMunge(text1, &lineArray, lineHash)
strIndexArray2 := dmp.diffLinesToStringsMunge(text2, &lineArray, lineHash)

return intArrayToString(strIndexArray1), intArrayToString(strIndexArray2), lineArray
}

// diffLinesToStringsMunge splits a text into an array of strings, and reduces the texts to a []string.
func (dmp *DiffMatchPatch) diffLinesToStringsMunge(text string, lineArray *[]string, lineHash map[string]int) []uint32 {
// Walk the text, pulling out a substring for each line. text.split('\n') would would temporarily double our memory footprint. Modifying text would create many large strings to garbage collect.
lineStart := 0
lineEnd := -1
strs := []uint32{}

for lineEnd < len(text)-1 {
lineEnd = indexOf(text, "\n", lineStart)

if lineEnd == -1 {
lineEnd = len(text) - 1
}

line := text[lineStart : lineEnd+1]
lineStart = lineEnd + 1
lineValue, ok := lineHash[line]

if ok {
strs = append(strs, uint32(lineValue))
} else {
*lineArray = append(*lineArray, line)
lineHash[line] = len(*lineArray) - 1
strs = append(strs, uint32(len(*lineArray)-1))
}
}

return strs
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify upstream version and known issues for vendored dependency.

This entire file is vendored third-party code from github.com/sergi/go-diff. A previous review identified a potential bug at line 756 where blanklineEndRegex is used instead of blanklineStartRegex for the boundary check on the right segment.

Since this is vendored code, modifications should come from upstream rather than being patched locally. Please verify:

  1. Whether this is the latest version of the library
  2. Whether the boundary regex issue has been fixed upstream
  3. If not, consider reporting the issue to the upstream maintainers

Run the following script to check the upstream status:


🏁 Script executed:

#!/bin/bash
# Check the latest version and tags of the upstream library
echo "=== Latest releases/tags from upstream ==="
gh api repos/sergi/go-diff/releases --jq '.[0:3] | .[] | {tag_name, published_at, name}'

echo -e "\n=== Check for open/closed issues related to blankline regex ==="
gh api graphql -f query='
{
  repository(owner: "sergi", name: "go-diff") {
    issues(first: 10, states: [OPEN, CLOSED], filterBy: {labels: ["bug"]}) {
      nodes {
        number
        title
        state
        createdAt
        url
      }
    }
  }
}'

echo -e "\n=== Recent commits mentioning blankline or semantic ==="
gh api repos/sergi/go-diff/commits --jq '.[0:10] | .[] | select(.commit.message | test("blankline|semantic"; "i")) | {sha: .sha[0:7], date: .commit.author.date, message: .commit.message}'

Length of output: 1870


Fix semantic cleanup regex bug at line 756.

The current code incorrectly uses blanklineEndRegex for both blankLine1 and blankLine2 checks. Since blankLine2 evaluates the beginning of the two parameter (checking the start), it should use blanklineStartRegex instead.

Line 756 should be:

blankLine2 := lineBreak2 && blanklineStartRegex.MatchString(two)

This bug exists in the current upstream repository and has not been reported or fixed. Since you've vendored this code, applying the fix locally is appropriate.

🤖 Prompt for AI Agents
In vendor/github.com/sergi/go-diff/diffmatchpatch/diff.go around lines ~740-760,
the semantic cleanup scoring uses blanklineEndRegex for both sides causing
blankLine2 to be computed against the wrong regex; change the computation for
blankLine2 to use blanklineStartRegex when evaluating the start of the second
string so blankLine2 := lineBreak2 && blanklineStartRegex.MatchString(two).
Ensure you only replace that single blankLine2 assignment and run tests to
confirm behavior.

Comment on lines +1 to +197
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

// The copyutil package contains libraries for copying directories of configuration.
package copyutil

import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/sergi/go-diff/diffmatchpatch"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/kustomize/kyaml/sets"
)

// CopyDir copies a src directory to a dst directory. CopyDir skips copying the .git directory from the src.
func CopyDir(fSys filesys.FileSystem, src string, dst string) error {
return errors.Wrap(fSys.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// don't copy the .git dir
if path != src {
rel := strings.TrimPrefix(path, src)
if IsDotGitFolder(rel) {
return nil
}
}

// path is an absolute path, rather than a path relative to src.
// e.g. if src is /path/to/package, then path might be /path/to/package/and/sub/dir
// we need the path relative to src `and/sub/dir` when we are copying the files to dest.
copyTo := strings.TrimPrefix(path, src)

// make directories that don't exist
if info.IsDir() {
return errors.Wrap(fSys.MkdirAll(filepath.Join(dst, copyTo)))
}

// copy file by reading and writing it
b, err := fSys.ReadFile(filepath.Join(src, copyTo))
if err != nil {
return errors.Wrap(err)
}
err = fSys.WriteFile(filepath.Join(dst, copyTo), b)
if err != nil {
return errors.Wrap(err)
}

return nil
}))
}

// Diff returns a list of files that differ between the source and destination.
//
// Diff is guaranteed to return a non-empty set if any files differ, but
// this set is not guaranteed to contain all differing files.
func Diff(sourceDir, destDir string) (sets.String, error) {
// get set of filenames in the package source
upstreamFiles := sets.String{}
err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// skip git repo if it exists
if IsDotGitFolder(path) {
return nil
}

upstreamFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, sourceDir), string(filepath.Separator)))
return nil
})
if err != nil {
return sets.String{}, err
}

// get set of filenames in the cloned package
localFiles := sets.String{}
err = filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// skip git repo if it exists
if IsDotGitFolder(path) {
return nil
}

localFiles.Insert(strings.TrimPrefix(strings.TrimPrefix(path, destDir), string(filepath.Separator)))
return nil
})
if err != nil {
return sets.String{}, err
}

// verify the source and cloned packages have the same set of filenames
diff := upstreamFiles.SymmetricDifference(localFiles)

// verify file contents match
for _, f := range upstreamFiles.Intersection(localFiles).List() {
fi, err := os.Stat(filepath.Join(destDir, f))
if err != nil {
return diff, err
}
if fi.Mode().IsDir() {
// already checked that this directory exists in the local files
continue
}

// compare upstreamFiles
b1, err := os.ReadFile(filepath.Join(destDir, f))
if err != nil {
return diff, err
}
b2, err := os.ReadFile(filepath.Join(sourceDir, f))
if err != nil {
return diff, err
}
if !bytes.Equal(b1, b2) {
fmt.Println(PrettyFileDiff(string(b1), string(b2)))
diff.Insert(f)
}
}
// return the differing files
return diff, nil
}

// IsDotGitFolder checks if the provided path is either the .git folder or
// a file underneath the .git folder.
func IsDotGitFolder(path string) bool {
cleanPath := filepath.ToSlash(filepath.Clean(path))
for _, c := range strings.Split(cleanPath, "/") {
if c == ".git" {
return true
}
}
return false
}

// PrettyFileDiff takes the content of two files and returns the pretty diff
func PrettyFileDiff(s1, s2 string) string {
dmp := diffmatchpatch.New()
wSrc, wDst, warray := dmp.DiffLinesToRunes(s1, s2)
diffs := dmp.DiffMainRunes(wSrc, wDst, false)
diffs = dmp.DiffCharsToLines(diffs, warray)
return dmp.DiffPrettyText(diffs)
}

// SyncFile copies file from src file path to a dst file path by replacement
// deletes dst file if src file doesn't exist
func SyncFile(src, dst string) error {
srcFileInfo, err := os.Stat(src)
if err != nil {
// delete dst if source doesn't exist
if err = deleteFile(dst); err != nil {
return err
}
return nil
}

input, err := os.ReadFile(src)
if err != nil {
return err
}

var filePerm os.FileMode

// get the destination file perm if file exists
dstFileInfo, err := os.Stat(dst)
if err != nil {
// get source file perm if destination file doesn't exist
filePerm = srcFileInfo.Mode().Perm()
} else {
filePerm = dstFileInfo.Mode().Perm()
}

err = os.WriteFile(dst, input, filePerm)
if err != nil {
return err
}

return nil
}

// deleteFile deletes file from path, returns no error if file doesn't exist
func deleteFile(path string) error {
if _, err := os.Stat(path); err != nil {
// return nil if file doesn't exist
return nil
}
return os.Remove(path)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify necessity and version of vendored copyutil package.

This entire file is newly vendored third-party code with identified security issues. Before merging:

  1. Confirm this package is required for the Metal3 migration functionality
  2. Verify you're vendoring the latest version of sigs.k8s.io/kustomize/kyaml
  3. Check if upstream has addressed the path traversal vulnerability
  4. Consider whether these utilities could be replaced with standard library functions or safer alternatives

🌐 Web query:

What is the latest version of sigs.k8s.io/kustomize/kyaml and does it fix the CopyDir path traversal vulnerability?

💡 Result:

Latest kyaml release: v0.20.1 (tag kyaml/v0.20.1, 23 Jul 2025). [1]

About the CopyDir path‑traversal issue: the path‑traversal vulnerabilities (GHSA-j77r-2fxf-5jrw / GHSA-7pwf-jg34-hxwp / CVE-2022-24877 / CVE-2022-24878) were fixed by introducing a new Kustomize filesystem that confines file access; the fix was shipped in kustomize-controller v0.24.0 (included in flux2 v0.29.0). Users are advised to upgrade to patched versions (and to a current kyaml). [2][3]

References

  • kustomize releases (kyaml/v0.20.1). [1]
  • CVE / advisory describing the path traversal and the fix (kustomize-controller v0.24.0 / flux2 v0.29.0). [2][3]


---

🏁 Script executed:

```shell
#!/bin/bash
# Check the kyaml version in go.mod and vendor metadata
echo "=== Checking go.mod for kyaml dependency ==="
if [ -f go.mod ]; then
  grep -i "sigs.k8s.io/kustomize/kyaml" go.mod || echo "kyaml not found in go.mod"
else
  echo "go.mod not found at repository root"
fi

echo ""
echo "=== Checking vendor/modules.txt ==="
if [ -f vendor/modules.txt ]; then
  grep "sigs.k8s.io/kustomize/kyaml" vendor/modules.txt | head -5
else
  echo "vendor/modules.txt not found"
fi

echo ""
echo "=== Checking for version info in vendor directory ==="
fd -t f "go.mod" vendor/sigs.k8s.io/kustomize/kyaml 2>/dev/null | head -3 | xargs -I {} sh -c 'echo "File: {}"; head -20 "{}"'

Length of output: 341



---

🌐 Web query:

```web
kustomize kyaml v0.19.0 security vulnerabilities path traversal CVE

💡 Result:

Short answer: Yes — kyaml v0.19.0 falls into the affected range for known Kustomize path‑traversal issues (CVE‑2022‑24877 and CVE‑2022‑24878). These were fixed in the kustomize controller / kustomize libraries by v0.29.0; upgrade to ≥ v0.29.0. [1][2][3]

Sources:

  • GitLab advisory / affected versions (starts at 0.19.0, fixed in 0.29.0). [1]
  • NVD CVE entries for CVE‑2022‑24877 / CVE‑2022‑24878 describing path‑traversal impacts and fixes. [2]
  • OSV / vulnerability record listing v0.19.0 in affected versions and fixed in v0.29.0. [3]

Would you like exact upgrade commands or checks for your project dependency graph?


Upgrade vendored kyaml from v0.19.0 to v0.29.0 to fix path traversal vulnerabilities.

The vendored kyaml v0.19.0 is affected by CVE-2022-24877 and CVE-2022-24878 path traversal vulnerabilities, which are fixed in v0.29.0. The CopyDir function in this file is part of the vulnerable code. Update the kyaml dependency to the patched version before merging.

🤖 Prompt for AI Agents
vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go lines 1-197: this
vendored kyaml copyutil contains the vulnerable CopyDir implementation from
kyaml v0.19.0; upgrade the kyaml dependency to v0.29.0 and re-vendor to pull in
the patched copyutil. Update go.mod to require sigs.k8s.io/kustomize/kyaml
v0.29.0 (or the minimal module that contains the fix), run `go get
sigs.k8s.io/kustomize/[email protected]` (or update replace directives if using
vendor), then run `go mod vendor` (or regenerate the vendor directory) so
vendor/sigs.k8s.io/kustomize/kyaml/... is replaced with the fixed code; run `go
test ./...` and a quick scan to ensure the new copyutil.go is present and no
other call sites need changes before committing.

Comment on lines +157 to +163
srcFileInfo, err := os.Stat(src)
if err != nil {
// delete dst if source doesn't exist
if err = deleteFile(dst); err != nil {
return err
}
return nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect error handling may delete destination on transient source errors.

The function deletes dst whenever os.Stat(src) fails, regardless of the error type. This could cause unintended data loss if src is temporarily inaccessible due to permissions, network issues, or other transient errors.

The code should check specifically for "not found" errors:

 	srcFileInfo, err := os.Stat(src)
 	if err != nil {
-		// delete dst if source doesn't exist
-		if err = deleteFile(dst); err != nil {
-			return err
+		// delete dst only if source doesn't exist
+		if os.IsNotExist(err) {
+			if err = deleteFile(dst); err != nil {
+				return err
+			}
+			return nil
 		}
-		return nil
+		return 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.

Suggested change
srcFileInfo, err := os.Stat(src)
if err != nil {
// delete dst if source doesn't exist
if err = deleteFile(dst); err != nil {
return err
}
return nil
srcFileInfo, err := os.Stat(src)
if err != nil {
// delete dst only if source doesn't exist
if os.IsNotExist(err) {
if err = deleteFile(dst); err != nil {
return err
}
return nil
}
return err
}
🤖 Prompt for AI Agents
In vendor/sigs.k8s.io/kustomize/kyaml/copyutil/copyutil.go around lines 157 to
163, the code unconditionally deletes dst whenever os.Stat(src) returns an
error; change this to only delete dst when the error indicates the source is
missing (use os.IsNotExist(err) or errors.Is(err, fs.ErrNotExist)), and for any
other stat errors return the error without modifying dst so transient
permission/network errors won't cause data loss.

@openshift-ci
Copy link
Contributor

openshift-ci bot commented Oct 29, 2025

@honza: all tests passed!

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.


fldPath := field.NewPath("spec")

mapaProviderConfig := baremetalv1.BareMetalMachineProviderSpec{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable name should probably be updated

warnings []string
)

mapaSpec, warn, err := m.toProviderSpec()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name needs updating

return nil, nil, nil, field.ErrorList{field.Invalid(field.NewPath("spec", "providerSpec", "value"), m.machine.Spec.ProviderSpec.Value, err.Error())}
}

capaMachine, warn, machineErrs := m.toMetal3Machine(baremetalProviderConfig)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name again


warnings = append(warnings, warn...)

capaMachine, ok := capaMachineObj.(*metal3v1.Metal3Machine)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants