Skip to content

Commit 194a1cb

Browse files
committed
🌱 (alpha update command): add e2e test
Adds a new e2e test for the alpha update command that validates custom code preservation during version updates.
1 parent 1948c3e commit 194a1cb

File tree

3 files changed

+320
-0
lines changed

3 files changed

+320
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package alphaupdate
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
. "github.com/onsi/ginkgo/v2"
24+
. "github.com/onsi/gomega"
25+
)
26+
27+
// Run e2e tests using the Ginkgo runner.
28+
func TestE2E(t *testing.T) {
29+
RegisterFailHandler(Fail)
30+
_, _ = fmt.Fprintf(GinkgoWriter, "Starting kubebuilder suite test for the alpha update command\n")
31+
RunSpecs(t, "Kubebuilder alpha update suite")
32+
}

test/e2e/alphaupdate/update_test.go

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package alphaupdate
18+
19+
import (
20+
"fmt"
21+
"io"
22+
"net/http"
23+
"os"
24+
"os/exec"
25+
"path/filepath"
26+
"runtime"
27+
28+
. "github.com/onsi/ginkgo/v2"
29+
. "github.com/onsi/gomega"
30+
pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
31+
"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
32+
)
33+
34+
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 + "-*"
41+
)
42+
43+
var _ = Describe("kubebuilder", func() {
44+
Context("alpha update", func() {
45+
var (
46+
mockProjectDir string
47+
binFromVersionPath string
48+
kbc *utils.TestContext
49+
)
50+
51+
BeforeEach(func() {
52+
var err error
53+
By("setting up test context with current kubebuilder binary")
54+
kbc, err = utils.NewTestContext(pluginutil.KubebuilderBinName, "GO111MODULE=on")
55+
Expect(err).NotTo(HaveOccurred())
56+
Expect(kbc.Prepare()).To(Succeed())
57+
58+
By("creating isolated mock project directory in /tmp to avoid git conflicts")
59+
mockProjectDir, err = os.MkdirTemp("/tmp", "kubebuilder-mock-project-")
60+
Expect(err).NotTo(HaveOccurred())
61+
62+
By("downloading kubebuilder v4.5.2 binary to isolated /tmp directory")
63+
binFromVersionPath, err = downloadKubebuilder()
64+
Expect(err).NotTo(HaveOccurred())
65+
})
66+
67+
AfterEach(func() {
68+
By("cleaning up test artifacts")
69+
70+
_ = os.RemoveAll(mockProjectDir)
71+
_ = os.RemoveAll(filepath.Dir(binFromVersionPath))
72+
73+
// Clean up kubebuilder alpha update downloaded binaries
74+
binaryPatterns := []string{
75+
binFromVersionPattern,
76+
binToVersionPattern,
77+
}
78+
79+
for _, pattern := range binaryPatterns {
80+
matches, _ := filepath.Glob(pattern)
81+
for _, path := range matches {
82+
_ = os.RemoveAll(path)
83+
}
84+
}
85+
86+
// Clean up TestContext
87+
if kbc != nil {
88+
kbc.Destroy()
89+
}
90+
})
91+
92+
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")
94+
createMockProject(mockProjectDir, binFromVersionPath)
95+
96+
By("injecting custom code in API and controller")
97+
injectCustomCode(mockProjectDir)
98+
99+
By("initializing git repository and committing mock project")
100+
initializeGitRepo(mockProjectDir)
101+
102+
By("running alpha update from v4.5.2 to v4.6.0")
103+
runAlphaUpdate(mockProjectDir, kbc)
104+
105+
By("validating custom code preservation")
106+
validateCustomCodePreservation(mockProjectDir)
107+
})
108+
})
109+
})
110+
111+
// 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-")
114+
if err != nil {
115+
return "", fmt.Errorf("failed to create binary directory: %w", err)
116+
}
117+
118+
url := fmt.Sprintf(
119+
"https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s",
120+
fromVersion,
121+
runtime.GOOS,
122+
runtime.GOARCH,
123+
)
124+
binaryPath := filepath.Join(binaryDir, "kubebuilder")
125+
126+
resp, err := http.Get(url)
127+
if err != nil {
128+
return "", fmt.Errorf("failed to download kubebuilder %s: %w", fromVersion, err)
129+
}
130+
defer func() { _ = resp.Body.Close() }()
131+
132+
if resp.StatusCode != http.StatusOK {
133+
return "", fmt.Errorf("failed to download kubebuilder %s: HTTP %d", fromVersion, resp.StatusCode)
134+
}
135+
136+
file, err := os.Create(binaryPath)
137+
if err != nil {
138+
return "", fmt.Errorf("failed to create binary file: %w", err)
139+
}
140+
defer func() { _ = file.Close() }()
141+
142+
_, err = io.Copy(file, resp.Body)
143+
if err != nil {
144+
return "", fmt.Errorf("failed to write binary: %w", err)
145+
}
146+
147+
err = os.Chmod(binaryPath, 0o755)
148+
if err != nil {
149+
return "", fmt.Errorf("failed to make binary executable: %w", err)
150+
}
151+
152+
return binaryPath, nil
153+
}
154+
155+
func createMockProject(projectDir, binaryPath string) {
156+
err := os.Chdir(projectDir)
157+
Expect(err).NotTo(HaveOccurred())
158+
159+
By("running kubebuilder init")
160+
cmd := exec.Command(binaryPath, "init", "--domain", "example.com", "--repo", "github.com/example/test-operator")
161+
cmd.Dir = projectDir
162+
_, err = cmd.CombinedOutput()
163+
Expect(err).NotTo(HaveOccurred())
164+
165+
By("running kubebuilder create api")
166+
cmd = exec.Command(
167+
binaryPath, "create", "api",
168+
"--group", "webapp",
169+
"--version", "v1",
170+
"--kind", "TestOperator",
171+
"--resource", "--controller",
172+
)
173+
cmd.Dir = projectDir
174+
_, err = cmd.CombinedOutput()
175+
Expect(err).NotTo(HaveOccurred())
176+
177+
By("running make generate manifests")
178+
cmd = exec.Command("make", "generate", "manifests")
179+
cmd.Dir = projectDir
180+
_, err = cmd.CombinedOutput()
181+
Expect(err).NotTo(HaveOccurred())
182+
}
183+
184+
func injectCustomCode(projectDir string) {
185+
typesFile := filepath.Join(projectDir, "api", "v1", "testoperator_types.go")
186+
err := pluginutil.InsertCode(
187+
typesFile,
188+
"Foo string `json:\"foo,omitempty\"`",
189+
`
190+
// +kubebuilder:validation:Minimum=0
191+
// +kubebuilder:validation:Maximum=3
192+
// +kubebuilder:default=1
193+
// Size is the size of the memcached deployment
194+
Size int32 `+"`json:\"size,omitempty\"`",
195+
)
196+
Expect(err).NotTo(HaveOccurred())
197+
controllerFile := filepath.Join(projectDir, "internal", "controller", "testoperator_controller.go")
198+
err = pluginutil.InsertCode(
199+
controllerFile,
200+
"// TODO(user): your logic here",
201+
`// Custom reconciliation logic
202+
log := ctrl.LoggerFrom(ctx)
203+
log.Info("Reconciling TestOperator")
204+
205+
// Fetch the TestOperator instance
206+
testOperator := &webappv1.TestOperator{}
207+
err := r.Get(ctx, req.NamespacedName, testOperator)
208+
if err != nil {
209+
return ctrl.Result{}, client.IgnoreNotFound(err)
210+
}
211+
212+
// Custom logic: log the size field
213+
log.Info("TestOperator size", "size", testOperator.Spec.Size)`,
214+
)
215+
Expect(err).NotTo(HaveOccurred())
216+
}
217+
218+
func initializeGitRepo(projectDir string) {
219+
By("initializing git repository")
220+
cmd := exec.Command("git", "init")
221+
cmd.Dir = projectDir
222+
_, err := cmd.CombinedOutput()
223+
Expect(err).NotTo(HaveOccurred())
224+
225+
cmd = exec.Command("git", "config", "user.email", "test@example.com")
226+
cmd.Dir = projectDir
227+
_, err = cmd.CombinedOutput()
228+
Expect(err).NotTo(HaveOccurred())
229+
230+
cmd = exec.Command("git", "config", "user.name", "Test User")
231+
cmd.Dir = projectDir
232+
_, err = cmd.CombinedOutput()
233+
Expect(err).NotTo(HaveOccurred())
234+
235+
By("adding all files to git")
236+
cmd = exec.Command("git", "add", "-A")
237+
cmd.Dir = projectDir
238+
_, err = cmd.CombinedOutput()
239+
Expect(err).NotTo(HaveOccurred())
240+
241+
By("committing initial project state")
242+
cmd = exec.Command("git", "commit", "-m", "Initial project with custom code")
243+
cmd.Dir = projectDir
244+
_, err = cmd.CombinedOutput()
245+
Expect(err).NotTo(HaveOccurred())
246+
247+
By("ensuring main branch exists and is current")
248+
cmd = exec.Command("git", "checkout", "-b", "main")
249+
cmd.Dir = projectDir
250+
_, err = cmd.CombinedOutput()
251+
if err != nil {
252+
// If main branch already exists, just switch to it
253+
cmd = exec.Command("git", "checkout", "main")
254+
cmd.Dir = projectDir
255+
_, err = cmd.CombinedOutput()
256+
Expect(err).NotTo(HaveOccurred())
257+
}
258+
}
259+
260+
func runAlphaUpdate(projectDir string, kbc *utils.TestContext) {
261+
err := os.Chdir(projectDir)
262+
Expect(err).NotTo(HaveOccurred())
263+
264+
// Use TestContext to run alpha update command
265+
cmd := exec.Command(kbc.BinaryName, "alpha", "update",
266+
"--from-version", fromVersion, "--to-version", toVersion, "--from-branch", "main")
267+
cmd.Dir = projectDir
268+
output, err := cmd.CombinedOutput()
269+
Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Alpha update failed: %s", string(output)))
270+
}
271+
272+
func validateCustomCodePreservation(projectDir string) {
273+
typesFile := filepath.Join(projectDir, "api", "v1", "testoperator_types.go")
274+
content, err := os.ReadFile(typesFile)
275+
Expect(err).NotTo(HaveOccurred())
276+
Expect(string(content)).To(ContainSubstring("Size int32 `json:\"size,omitempty\"`"))
277+
Expect(string(content)).To(ContainSubstring("Size is the size of the memcached deployment"))
278+
279+
controllerFile := filepath.Join(projectDir, "internal", "controller", "testoperator_controller.go")
280+
content, err = os.ReadFile(controllerFile)
281+
Expect(err).NotTo(HaveOccurred())
282+
Expect(string(content)).To(ContainSubstring("Custom reconciliation logic"))
283+
Expect(string(content)).To(ContainSubstring("log.Info(\"Reconciling TestOperator\")"))
284+
Expect(string(content)).To(ContainSubstring("log.Info(\"TestOperator size\", \"size\", testOperator.Spec.Size)"))
285+
}

test/features.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ go test "$(dirname "$0")/e2e/grafana" ${flags:-} -timeout 30m
3131
header_text "Running Alpha Generate Command E2E tests"
3232
go test "$(dirname "$0")/e2e/alphagenerate" ${flags:-} -timeout 30m
3333

34+
header_text "Running Alpha Update Command E2E tests"
35+
go test "$(dirname "$0")/e2e/alphaupdate" ${flags:-} -timeout 30m
36+
3437
popd >/dev/null

0 commit comments

Comments
 (0)