Skip to content

Commit 1340dc8

Browse files
committed
✨ add alpha update command
This commit adds the alpha update command. Alpha update attempts to update the version of the project and keep custom code by performing a tree-way merge with a synthetic ancestor.
1 parent 06e3c2f commit 1340dc8

File tree

4 files changed

+428
-14
lines changed

4 files changed

+428
-14
lines changed

pkg/cli/alpha.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,8 @@ const (
2929
)
3030

3131
var alphaCommands = []*cobra.Command{
32-
newAlphaCommand(),
33-
alpha.NewScaffoldCommand(),
34-
}
35-
36-
func newAlphaCommand() *cobra.Command {
37-
cmd := &cobra.Command{
38-
// TODO: If we need to create alpha commands please add a new file for each command
39-
}
40-
return cmd
32+
alpha.NewGenerateCommand(),
33+
alpha.NewUpdateCommand(),
4134
}
4235

4336
func (c *CLI) newAlphaCmd() *cobra.Command {

pkg/cli/alpha/command.go renamed to pkg/cli/alpha/generate.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ import (
3434
//
3535
// Technically, implementing functions that allow re-scaffolding with the exact plugins and project-specific
3636
// code of external projects is not feasible within Kubebuilder’s current design.
37-
func NewScaffoldCommand() *cobra.Command {
37+
func NewGenerateCommand() *cobra.Command {
3838
opts := internal.Generate{}
39-
scaffoldCmd := &cobra.Command{
39+
generateCmd := &cobra.Command{
4040
Use: "generate",
4141
Short: "Re-scaffold an existing Kuberbuilder project",
4242
Long: `It's an experimental feature that has the purpose of re-scaffolding the whole project from the scratch
@@ -54,12 +54,12 @@ Then we will re-scaffold the project by Kubebuilder in the directory specified b
5454
}
5555
},
5656
}
57-
scaffoldCmd.Flags().StringVar(&opts.InputDir, "input-dir", "",
57+
generateCmd.Flags().StringVar(&opts.InputDir, "input-dir", "",
5858
"Specifies the full path to a Kubebuilder project file. If not provided, "+
5959
"the current working directory is used.")
60-
scaffoldCmd.Flags().StringVar(&opts.OutputDir, "output-dir", "",
60+
generateCmd.Flags().StringVar(&opts.OutputDir, "output-dir", "",
6161
"Specifies the full path where the scaffolded files will be output. "+
6262
"Defaults to a directory within the current working directory.")
6363

64-
return scaffoldCmd
64+
return generateCmd
6565
}

pkg/cli/alpha/internal/update.go

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"os"
8+
"os/exec"
9+
"runtime"
10+
"strings"
11+
12+
log "github.com/sirupsen/logrus"
13+
"github.com/spf13/afero"
14+
"sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml"
15+
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
16+
)
17+
18+
// Update contains configuration for the update operation
19+
type Update struct {
20+
// FromVersion specifies which version of Kubebuilder to use for the update.
21+
// If empty, the version from the PROJECT file will be used.
22+
FromVersion string
23+
}
24+
25+
// Update performs a complete project update by creating a three-way merge to help users
26+
// upgrade their Kubebuilder projects. The process creates multiple Git branches:
27+
// - ancestor: Clean state with old Kubebuilder version scaffolding
28+
// - current: User's current project state
29+
// - upgrade: New Kubebuilder version scaffolding
30+
// - merge: Attempts to merge upgrade changes into current state
31+
func (opts *Update) Update() error {
32+
// Load the PROJECT configuration file to get the current CLI version
33+
projectConfigFile := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()})
34+
if err := projectConfigFile.LoadFrom(yaml.DefaultPath); err != nil { // TODO: assess if DefaultPath could be renamed to a more self-descriptive name
35+
return fmt.Errorf("fail to run command: %w", err)
36+
}
37+
38+
// Determine which Kubebuilder version to use for the update
39+
cliVersion := projectConfigFile.Config().GetCliVersion()
40+
41+
// Allow override of the version from PROJECT file via command line flag
42+
if opts.FromVersion != "" {
43+
// Ensure version has 'v' prefix for consistency with GitHub releases
44+
if !strings.HasPrefix(opts.FromVersion, "v") {
45+
opts.FromVersion = "v" + opts.FromVersion
46+
}
47+
log.Infof("Overriding cliVersion field %s from PROJECT file with --from-version %s", cliVersion, opts.FromVersion)
48+
cliVersion = opts.FromVersion
49+
} else {
50+
log.Infof("Using CLI version from PROJECT file: %s", cliVersion)
51+
}
52+
53+
// Download the specific Kubebuilder binary version for generating clean scaffolding
54+
tempDir, err := opts.downloadKubebuilderBinary(cliVersion)
55+
if err != nil {
56+
return fmt.Errorf("failed to download Kubebuilder %s binary: %w", cliVersion, err)
57+
}
58+
log.Infof("Downloaded binary kept at %s for debugging purposes", tempDir)
59+
60+
// Create ancestor branch with clean state for three-way merge
61+
if err := opts.checkoutAncestorBranch(); err != nil {
62+
return fmt.Errorf("failed to checkout the ancestor branch: %w", err)
63+
}
64+
65+
// Remove all existing files to create a clean slate for re-scaffolding
66+
if err := opts.cleanUpAncestorBranch(); err != nil {
67+
return fmt.Errorf("failed to clean up the ancestor branch: %w", err)
68+
}
69+
70+
// Generate clean scaffolding using the old Kubebuilder version
71+
if err := opts.runAlphaGenerate(tempDir, cliVersion); err != nil {
72+
return fmt.Errorf("failed to run alpha generate on ancestor branch: %w", err)
73+
}
74+
75+
// Create current branch representing user's existing project state
76+
if err := opts.checkoutCurrentOffAncestor(); err != nil {
77+
return fmt.Errorf("failed to checkout current off ancestor: %w", err)
78+
}
79+
80+
// Create upgrade branch with new Kubebuilder version scaffolding
81+
if err := opts.checkoutUpgradeOffAncestor(); err != nil {
82+
return fmt.Errorf("failed to checkout upgrade off ancestor: %w", err)
83+
}
84+
85+
// Create merge branch to attempt automatic merging of changes
86+
if err := opts.checkoutMergeOffCurrent(); err != nil {
87+
return fmt.Errorf("failed to checkout merge branch off current: %w", err)
88+
}
89+
90+
// Attempt to merge upgrade changes into the user's current state
91+
if err := opts.mergeUpgradeIntoMerge(); err != nil {
92+
return fmt.Errorf("failed to merge upgrade into merge branch: %w", err)
93+
}
94+
95+
return nil
96+
}
97+
98+
// downloadKubebuilderBinary downloads the specified version of Kubebuilder binary
99+
// from GitHub releases and saves it to a temporary directory with executable permissions.
100+
// Returns the temporary directory path containing the binary.
101+
func (opts *Update) downloadKubebuilderBinary(version string) (string, error) {
102+
cliVersion := version
103+
104+
// Construct GitHub release URL based on current OS and architecture
105+
url := fmt.Sprintf("https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s",
106+
cliVersion, runtime.GOOS, runtime.GOARCH)
107+
108+
log.Infof("Downloading the Kubebuilder %s binary from: %s", cliVersion, url)
109+
110+
// Create temporary directory for storing the downloaded binary
111+
fs := afero.NewOsFs()
112+
tempDir, err := afero.TempDir(fs, "", "kubebuilder"+cliVersion+"-")
113+
if err != nil {
114+
return "", fmt.Errorf("failed to create temporary directory: %w", err)
115+
}
116+
117+
// Create the binary file in the temporary directory
118+
binaryPath := tempDir + "/kubebuilder"
119+
file, err := os.Create(binaryPath)
120+
if err != nil {
121+
return "", fmt.Errorf("failed to create the binary file: %w", err)
122+
}
123+
defer file.Close()
124+
125+
// Download the binary from GitHub releases
126+
response, err := http.Get(url)
127+
if err != nil {
128+
return "", fmt.Errorf("failed to download the binary: %w", err)
129+
}
130+
defer response.Body.Close()
131+
132+
// Check if download was successful
133+
if response.StatusCode != http.StatusOK {
134+
return "", fmt.Errorf("failed to download the binary: HTTP %d", response.StatusCode)
135+
}
136+
137+
// Copy the downloaded content to the local file
138+
_, err = io.Copy(file, response.Body)
139+
if err != nil {
140+
return "", fmt.Errorf("failed to write the binary content to file: %w", err)
141+
}
142+
143+
// Make the binary executable
144+
if err := os.Chmod(binaryPath, 0755); err != nil {
145+
return "", fmt.Errorf("failed to make binary executable: %w", err)
146+
}
147+
148+
log.Infof("Kubebuilder version %s succesfully downloaded to %s", cliVersion, binaryPath)
149+
150+
return tempDir, nil
151+
}
152+
153+
// checkoutAncestorBranch creates and switches to the 'ancestor' branch.
154+
// This branch will serve as the common ancestor for the three-way merge,
155+
// containing clean scaffolding from the old Kubebuilder version.
156+
func (opts *Update) checkoutAncestorBranch() error {
157+
gitCmd := exec.Command("git", "checkout", "-b", "ancestor")
158+
if err := gitCmd.Run(); err != nil {
159+
return fmt.Errorf("failed to create and checkout ancestor branch: %w", err)
160+
}
161+
log.Info("Created and checked out ancestor branch")
162+
163+
return nil
164+
}
165+
166+
// cleanUpAncestorBranch removes all files from the ancestor branch to create
167+
// a clean state for re-scaffolding. This ensures the ancestor branch only
168+
// contains pure scaffolding without any user modifications.
169+
func (opts *Update) cleanUpAncestorBranch() error {
170+
// Remove all tracked files from the Git repository
171+
gitCmd := exec.Command("git", "rm", "-rf", ".")
172+
if err := gitCmd.Run(); err != nil {
173+
return fmt.Errorf("failed to remove tracked files in ancestor branch: %w", err)
174+
}
175+
log.Info("Successfully removed tracked files from ancestor branch")
176+
177+
// Remove all untracked files and directories
178+
gitCmd = exec.Command("git", "clean", "-fd")
179+
if err := gitCmd.Run(); err != nil {
180+
return fmt.Errorf("failed to clean untracked files: %w", err)
181+
}
182+
log.Info("Successfully cleaned untracked files from ancestor branch")
183+
184+
// Commit the cleanup to establish the clean state
185+
gitCmd = exec.Command("git", "commit", "-m", "Clean up the ancestor branch")
186+
if err := gitCmd.Run(); err != nil {
187+
return fmt.Errorf("failed to commit the cleanup in ancestor branch: %w", err)
188+
}
189+
log.Info("Successfully committed cleanup on ancestor")
190+
191+
return nil
192+
}
193+
194+
// runAlphaGenerate executes the old Kubebuilder version's 'alpha generate' command
195+
// to create clean scaffolding in the ancestor branch. This uses the downloaded
196+
// binary with the original PROJECT file to recreate the project's initial state.
197+
func (opts *Update) runAlphaGenerate(tempDir, version string) error {
198+
tempBinaryPath := tempDir + "/kubebuilder"
199+
200+
// Temporarily modify PATH to use the downloaded Kubebuilder binary
201+
originalPath := os.Getenv("PATH")
202+
tempEnvPath := tempDir + ":" + originalPath
203+
204+
if err := os.Setenv("PATH", tempEnvPath); err != nil {
205+
return fmt.Errorf("failed to set temporary PATH: %w", err)
206+
}
207+
// Restore original PATH when function completes
208+
defer func() {
209+
if err := os.Setenv("PATH", originalPath); err != nil {
210+
log.Errorf("failed to restore original PATH: %w", err)
211+
}
212+
}()
213+
214+
// Prepare the alpha generate command with proper I/O redirection
215+
cmd := exec.Command(tempBinaryPath, "alpha", "generate")
216+
cmd.Stdout = os.Stdout
217+
cmd.Stderr = os.Stderr
218+
cmd.Env = os.Environ()
219+
220+
// Restore the original PROJECT file from master branch to ensure
221+
// we're using the correct project configuration for scaffolding
222+
gitCmd := exec.Command("git", "checkout", "master", "--", "PROJECT")
223+
if err := gitCmd.Run(); err != nil {
224+
return fmt.Errorf("failed to checkout PROJECT from master")
225+
}
226+
log.Info("Succesfully checked out the PROJECT file from master branch")
227+
228+
// Execute the alpha generate command to create clean scaffolding
229+
if err := cmd.Run(); err != nil {
230+
return fmt.Errorf("failed to run alpha generate: %w", err)
231+
}
232+
log.Info("Successfully ran alpha generate using Kubebuilder ", version)
233+
234+
// Stage all generated files
235+
gitCmd = exec.Command("git", "add", ".")
236+
if err := gitCmd.Run(); err != nil {
237+
return fmt.Errorf("failed to stage changes in ancestor: %w", err)
238+
}
239+
log.Info("Successfully staged all changes in ancestor")
240+
241+
// Commit the re-scaffolded project to the ancestor branch
242+
gitCmd = exec.Command("git", "commit", "-m", "Re-scaffold in ancestor")
243+
if err := gitCmd.Run(); err != nil {
244+
return fmt.Errorf("failed to commit changes in ancestor: %w", err)
245+
}
246+
log.Info("Successfully commited changes in ancestor")
247+
248+
return nil
249+
}
250+
251+
// checkoutCurrentOffAncestor creates the 'current' branch from ancestor and
252+
// populates it with the user's actual project content from the master branch.
253+
// This represents the current state of the user's project.
254+
func (opts *Update) checkoutCurrentOffAncestor() error {
255+
// Create current branch starting from the clean ancestor state
256+
gitCmd := exec.Command("git", "checkout", "-b", "current", "ancestor")
257+
if err := gitCmd.Run(); err != nil {
258+
return fmt.Errorf("failed to checkout current branch off ancestor: %w", err)
259+
}
260+
log.Info("Successfully checked out current branch off ancestor")
261+
262+
// Overlay the user's actual project content from master branch
263+
gitCmd = exec.Command("git", "checkout", "master", "--", ".")
264+
if err := gitCmd.Run(); err != nil {
265+
return fmt.Errorf("failed to checkout content from master onto current: %w", err)
266+
}
267+
log.Info("Successfully checked out content from main onto current branch")
268+
269+
// Stage all the user's current project content
270+
gitCmd = exec.Command("git", "add", ".")
271+
if err := gitCmd.Run(); err != nil {
272+
return fmt.Errorf("failed to stage all changes in current: %w", err)
273+
}
274+
log.Info("Successfully staged all changes in current")
275+
276+
// Commit the user's current state to the current branch
277+
gitCmd = exec.Command("git", "commit", "-m", "Add content from main onto current branch")
278+
if err := gitCmd.Run(); err != nil {
279+
return fmt.Errorf("failed to commit changes: %w", err)
280+
}
281+
log.Info("Successfully commited changes in current")
282+
283+
return nil
284+
}
285+
286+
// checkoutUpgradeOffAncestor creates the 'upgrade' branch from ancestor and
287+
// generates fresh scaffolding using the current (latest) Kubebuilder version.
288+
// This represents what the project should look like with the new version.
289+
func (opts *Update) checkoutUpgradeOffAncestor() error {
290+
// Create upgrade branch starting from the clean ancestor state
291+
gitCmd := exec.Command("git", "checkout", "-b", "upgrade", "ancestor")
292+
if err := gitCmd.Run(); err != nil {
293+
return fmt.Errorf("failed to checkout upgrade branch off ancestor: %w", err)
294+
}
295+
log.Info("Successfully checked out upgrade branch off ancestor")
296+
297+
// Run alpha generate with the current (new) Kubebuilder version
298+
// This uses the system's installed kubebuilder binary
299+
cmd := exec.Command("kubebuilder", "alpha", "generate")
300+
cmd.Stdout = os.Stdout
301+
cmd.Stderr = os.Stderr
302+
303+
if err := cmd.Run(); err != nil {
304+
return fmt.Errorf("failed to run alpha generate on upgrade branch: %w", err)
305+
}
306+
log.Info("Successfully ran alpha generate on upgrade branch")
307+
308+
// Stage all the newly generated files
309+
gitCmd = exec.Command("git", "add", ".")
310+
if err := gitCmd.Run(); err != nil {
311+
return fmt.Errorf("failed to stage changes on upgrade: %w", err)
312+
}
313+
log.Info("Successfully staged all changes in upgrade branch")
314+
315+
// Commit the new version's scaffolding to the upgrade branch
316+
gitCmd = exec.Command("git", "commit", "-m", "alpha generate in upgrade branch")
317+
if err := gitCmd.Run(); err != nil {
318+
return fmt.Errorf("failed to commit changes in upgrade branch: %w", err)
319+
}
320+
log.Info("Successfully commited changes in upgrade branch")
321+
322+
return nil
323+
}
324+
325+
// checkoutMergeOffCurrent creates the 'merge' branch from the current branch.
326+
// This branch will be used to attempt automatic merging of upgrade changes
327+
// with the user's current project state.
328+
func (opts *Update) checkoutMergeOffCurrent() error {
329+
gitCmd := exec.Command("git", "checkout", "-b", "merge", "current")
330+
if err := gitCmd.Run(); err != nil {
331+
return fmt.Errorf("failed to checkout merge branch off current: %w", err)
332+
}
333+
334+
return nil
335+
}
336+
337+
// mergeUpgradeIntoMerge attempts to merge the upgrade branch (containing new
338+
// Kubebuilder scaffolding) into the merge branch (containing user's current state).
339+
// If conflicts occur, it warns the user to resolve them manually rather than failing.
340+
func (opts *Update) mergeUpgradeIntoMerge() error {
341+
gitCmd := exec.Command("git", "merge", "upgrade")
342+
err := gitCmd.Run()
343+
if err != nil {
344+
// Check if the error is due to merge conflicts (exit code 1)
345+
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
346+
log.Warn("Merge with conflicts. Please resolve them manually")
347+
return nil // Don't treat conflicts as fatal errors
348+
}
349+
return fmt.Errorf("failed to merge the upgrade branch into the merge branch: %w", err)
350+
}
351+
352+
return nil
353+
}

0 commit comments

Comments
 (0)