Skip to content

Commit bef2529

Browse files
committed
add --force flag to alpha update command
This commit adds the --force flag to the alpha update command. It also adds e2e tests and documentation for the flag. The --force flag makes it possible to run the alpha update command in CI workflows.
1 parent dbd035c commit bef2529

File tree

4 files changed

+145
-22
lines changed

4 files changed

+145
-22
lines changed

docs/book/src/reference/commands/alpha_update.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ kubebuilder alpha update \
6161
--from-branch=main
6262
```
6363

64+
Force update even with merge conflicts:
65+
66+
```sh
67+
kubebuilder alpha update --force
68+
```
69+
6470
<aside class="note warning">
6571
<h1>You might need to upgrade your project first</h1>
6672

@@ -81,8 +87,46 @@ Once updated, you can use `kubebuilder alpha update` for future upgrades.
8187
| `--from-version` | **Required for projects initialized with versions earlier than v4.6.0.** Kubebuilder version your project was created with. If unset, uses the `PROJECT` file. |
8288
| `--to-version` | Version to upgrade to. Defaults to the latest version. |
8389
| `--from-branch` | Git branch that contains your current project code. Defaults to `main`. |
90+
| `--force` | Force the update even if conflicts occur. Conflicted files will include conflict markers, and a commit will be created automatically. Ideal for automation (e.g., cronjobs, CI). |
8491
| `-h, --help` | Show help for this command. |
8592

93+
## Using the `--force` flag
94+
95+
When using the `--force` flag, merge conflicts will be committed with conflict markers in the `tmp-kb-update-merge` branch.
96+
You should carefully review and resolve these conflicts before merging into your main branch.
97+
98+
After resolving conflicts, run the following command to ensure manifests and generated files are up to date:
99+
```sh
100+
make manifests generate fmt vet lint-fix
101+
```
102+
103+
You might want to run `make all`, ensure that all tests are passing, and thoroughly validate the final result before committing and pushing a pull request to update your project from the `tmp-kb-update-merge` branch.
104+
105+
## Handling Merge Conflicts
106+
107+
If conflicts occur during a merge, Git will stop the process and leave the merge branch in a conflicted state for manual resolution.
108+
109+
To proceed with the merge despite conflicts, you can use the `--force` option. This is useful in automated environments, such as CI pipelines or cron jobs, where you want to create a pull request with the changes - even if conflicts are present.
110+
111+
```
112+
kubebuilder alpha update --force
113+
```
114+
The resulting commit will include conflict markers in the affected files:
115+
116+
```
117+
<<<<<<< HEAD
118+
Your changes
119+
=======
120+
Incoming changes
121+
>>>>>>> branch-name
122+
```
123+
124+
The commit message will indicate that conflicts are present and need to be resolved manually.
125+
126+
<aside>
127+
Note: This approach is typically used in automation workflows where conflict markers are later addressed by a human, or where preserving the conflicting changes is acceptable for follow-up processing.
128+
</aside>
129+
86130
<aside class="note warning">
87131
<h1>Projects generated with </h1>
88132

pkg/cli/alpha/internal/update/update.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type Update struct {
3838
ToVersion string
3939
// FromBranch stores the branch to update from, e.g., "main".
4040
FromBranch string
41+
// Force commits the update changes even with merge conflicts
42+
Force bool
4143

4244
// UpdateBranches
4345
AncestorBranch string
@@ -334,8 +336,15 @@ func (opts *Update) mergeOriginalToUpgrade() error {
334336
var exitErr *exec.ExitError
335337
// If the merge has an error that is not a conflict, return an error 2
336338
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
337-
log.Warn("Merge completed with conflicts. Manual resolution required.")
338339
hasConflicts = true
340+
if !opts.Force {
341+
log.Warn("Merge stopped due to conflicts. Manual resolution is required.")
342+
log.Warn("After resolving the conflicts, run the following command:")
343+
log.Warn(" make manifests generate fmt vet lint-fix")
344+
log.Warn("This ensures manifests and generated files are up to date, and the project layout remains consistent.")
345+
return fmt.Errorf("merge stopped due to conflicts")
346+
}
347+
log.Warn("Merge completed with conflicts. Conflict markers will be committed.")
339348
} else {
340349
return fmt.Errorf("merge failed unexpectedly: %w", err)
341350
}

pkg/cli/alpha/update.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ The process uses Git branches:
4949
- upgrade: scaffold from the target version
5050
- merge: result of the 3-way merge
5151
52-
If conflicts occur during the merge, resolve them manually in the 'merge' branch.
53-
Once resolved, commit and push it as a pull request. This branch will contain the
54-
final upgraded project with the latest Kubebuilder layout and your custom code.
52+
If conflicts occur during the merge, the command will stop and leave the merge branch for manual resolution.
53+
Use --force to commit conflicts with markers instead.
5554
5655
Examples:
5756
# Update from the version specified in the PROJECT file to the latest release
@@ -63,6 +62,9 @@ Examples:
6362
# Update from a specific version to an specific release
6463
kubebuilder alpha update --from-version v4.5.0 --to-version v4.7.0
6564
65+
# Force update even with merge conflicts (commit conflict markers)
66+
kubebuilder alpha update --force
67+
6668
`,
6769

6870
PreRunE: func(_ *cobra.Command, _ []string) error {
@@ -92,5 +94,9 @@ Examples:
9294
updateCmd.Flags().StringVar(&opts.FromBranch, "from-branch", "",
9395
"Git branch to use as current state of the project for the update.")
9496

97+
updateCmd.Flags().BoolVar(&opts.Force, "force", false,
98+
"Force the update even if conflicts occur. Conflicted files will include conflict markers, and a "+
99+
"commit will be created automatically. Ideal for automation (e.g., cronjobs, CI).")
100+
95101
return updateCmd
96102
}

test/e2e/alphaupdate/update_test.go

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,9 @@ import (
3232
)
3333

3434
const (
35-
fromVersion = "v4.5.2"
36-
toVersion = "v4.6.0"
37-
38-
// Binary patterns for cleanup
39-
binFromVersionPattern = "/tmp/kubebuilder" + fromVersion + "-*"
40-
binToVersionPattern = "/tmp/kubebuilder" + toVersion + "-*"
35+
fromVersionWithoutConflicts = "v4.5.2"
36+
fromVersionWithConflicts = "v4.5.0"
37+
toVersion = "v4.6.0"
4138
)
4239

4340
var _ = Describe("kubebuilder", func() {
@@ -59,8 +56,8 @@ var _ = Describe("kubebuilder", func() {
5956
mockProjectDir, err = os.MkdirTemp("/tmp", "kubebuilder-mock-project-")
6057
Expect(err).NotTo(HaveOccurred())
6158

62-
By("downloading kubebuilder v4.5.2 binary to isolated /tmp directory")
63-
binFromVersionPath, err = downloadKubebuilder()
59+
By("downloading kubebuilder binary to isolated /tmp directory")
60+
binFromVersionPath, err = downloadKubebuilderVersion(fromVersionWithoutConflicts)
6461
Expect(err).NotTo(HaveOccurred())
6562
})
6663

@@ -72,8 +69,9 @@ var _ = Describe("kubebuilder", func() {
7269

7370
// Clean up kubebuilder alpha update downloaded binaries
7471
binaryPatterns := []string{
75-
binFromVersionPattern,
76-
binToVersionPattern,
72+
"/tmp/kubebuilder" + fromVersionWithoutConflicts + "-*",
73+
"/tmp/kubebuilder" + fromVersionWithConflicts + "-*",
74+
"/tmp/kubebuilder" + toVersion + "-*",
7775
}
7876

7977
for _, pattern := range binaryPatterns {
@@ -90,7 +88,7 @@ var _ = Describe("kubebuilder", func() {
9088
})
9189

9290
It("should update project from v4.5.2 to v4.6.0 preserving custom code", func() {
93-
By("creating mock project with kubebuilder v4.5.2")
91+
By("creating mock project with kubebuilder " + fromVersionWithoutConflicts)
9492
createMockProject(mockProjectDir, binFromVersionPath)
9593

9694
By("injecting custom code in API and controller")
@@ -99,38 +97,87 @@ var _ = Describe("kubebuilder", func() {
9997
By("initializing git repository and committing mock project")
10098
initializeGitRepo(mockProjectDir)
10199

102-
By("running alpha update from v4.5.2 to v4.6.0")
100+
By("running alpha update from " + fromVersionWithoutConflicts + " to " + toVersion)
103101
runAlphaUpdate(mockProjectDir, kbc)
104102

105103
By("validating custom code preservation")
106104
validateCustomCodePreservation(mockProjectDir)
107105
})
106+
107+
It("should stop on merge conflicts when --force is not used", func() {
108+
// Download the conflicts version binary for this test
109+
conflictsBinPath, err := downloadKubebuilderVersion(fromVersionWithConflicts)
110+
Expect(err).NotTo(HaveOccurred())
111+
defer func() { _ = os.RemoveAll(filepath.Dir(conflictsBinPath)) }()
112+
113+
By("creating mock project with kubebuilder " + fromVersionWithConflicts)
114+
createMockProject(mockProjectDir, conflictsBinPath)
115+
116+
By("initializing git repository and committing mock project")
117+
initializeGitRepo(mockProjectDir)
118+
119+
By("running alpha update without --force flag")
120+
cmd := exec.Command(kbc.BinaryName, "alpha", "update",
121+
"--from-version", fromVersionWithConflicts, "--to-version", toVersion, "--from-branch", "main")
122+
cmd.Dir = mockProjectDir
123+
output, err := cmd.CombinedOutput()
124+
125+
By("expecting the command to fail due to merge conflicts")
126+
Expect(err).To(HaveOccurred())
127+
Expect(string(output)).To(ContainSubstring("Merge stopped due to conflicts"))
128+
Expect(string(output)).To(ContainSubstring("merge stopped due to conflicts"))
129+
})
130+
131+
It("should commit with conflict markers when --force is used", func() {
132+
// Download the conflicts version binary for this test
133+
conflictsBinPath, err := downloadKubebuilderVersion(fromVersionWithConflicts)
134+
Expect(err).NotTo(HaveOccurred())
135+
defer func() { _ = os.RemoveAll(filepath.Dir(conflictsBinPath)) }()
136+
137+
By("creating mock project with kubebuilder " + fromVersionWithConflicts)
138+
createMockProject(mockProjectDir, conflictsBinPath)
139+
140+
By("initializing git repository and committing mock project")
141+
initializeGitRepo(mockProjectDir)
142+
143+
By("running alpha update with --force flag")
144+
cmd := exec.Command(kbc.BinaryName, "alpha", "update",
145+
"--from-version", fromVersionWithConflicts, "--to-version", toVersion, "--from-branch", "main", "--force")
146+
cmd.Dir = mockProjectDir
147+
output, err := cmd.CombinedOutput()
148+
149+
By("expecting the command to succeed despite conflicts")
150+
Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Alpha update with --force failed: %s", string(output)))
151+
152+
By("validating that conflict markers are present in the merged files")
153+
validateConflictMarkers(mockProjectDir)
154+
})
108155
})
109156
})
110157

111158
// downloadKubebuilder downloads the --from-version kubebuilder binary to a temporary directory
112-
func downloadKubebuilder() (string, error) {
113-
binaryDir, err := os.MkdirTemp("", "kubebuilder-v4.5.2-")
159+
func downloadKubebuilderVersion(version string) (string, error) {
160+
binaryDir, err := os.MkdirTemp("", "kubebuilder-"+version+"-")
114161
if err != nil {
115162
return "", fmt.Errorf("failed to create binary directory: %w", err)
116163
}
117164

118165
url := fmt.Sprintf(
119166
"https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s",
120-
fromVersion,
167+
version,
121168
runtime.GOOS,
122169
runtime.GOARCH,
123170
)
124171
binaryPath := filepath.Join(binaryDir, "kubebuilder")
125172

126173
resp, err := http.Get(url)
127174
if err != nil {
128-
return "", fmt.Errorf("failed to download kubebuilder %s: %w", fromVersion, err)
175+
return "", fmt.Errorf("failed to download kubebuilder %s: %w", version, err)
129176
}
130177
defer func() { _ = resp.Body.Close() }()
131178

132179
if resp.StatusCode != http.StatusOK {
133-
return "", fmt.Errorf("failed to download kubebuilder %s: HTTP %d", fromVersion, resp.StatusCode)
180+
return "", fmt.Errorf("failed to download kubebuilder %s: HTTP %d", version, resp.StatusCode)
134181
}
135182

136183
file, err := os.Create(binaryPath)
@@ -263,7 +310,7 @@ func runAlphaUpdate(projectDir string, kbc *utils.TestContext) {
263310

264311
// Use TestContext to run alpha update command
265312
cmd := exec.Command(kbc.BinaryName, "alpha", "update",
266-
"--from-version", fromVersion, "--to-version", toVersion, "--from-branch", "main")
313+
"--from-version", fromVersionWithoutConflicts, "--to-version", toVersion, "--from-branch", "main")
267314
cmd.Dir = projectDir
268315
output, err := cmd.CombinedOutput()
269316
Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Alpha update failed: %s", string(output)))
@@ -283,3 +330,20 @@ func validateCustomCodePreservation(projectDir string) {
283330
Expect(string(content)).To(ContainSubstring("log.Info(\"Reconciling TestOperator\")"))
284331
Expect(string(content)).To(ContainSubstring("log.Info(\"TestOperator size\", \"size\", testOperator.Spec.Size)"))
285332
}
333+
334+
func validateConflictMarkers(projectDir string) {
335+
// Use git to check for conflict markers across the entire project
336+
cmd := exec.Command("git", "grep", "-l", "^<<<<<<<\\|^=======\\|^>>>>>>>", ".")
337+
cmd.Dir = projectDir
338+
output, err := cmd.CombinedOutput()
339+
340+
// If git grep finds conflict markers, it returns exit code 0
341+
// If no conflict markers are found, it returns exit code 1
342+
if err == nil {
343+
// Conflict markers found - this is expected
344+
Expect(output).ToNot(BeEmpty(), "Expected conflict markers in merged files")
345+
} else {
346+
// No conflict markers found
347+
Expect(false).To(BeTrue(), "Expected conflict markers in merged files but none were found")
348+
}
349+
}

0 commit comments

Comments
 (0)