From a78ee586952090aeeed54fc892a89b2a30545b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 1 Apr 2026 13:02:15 +0200 Subject: [PATCH 1/9] Add func CLI wrapper functions and refactor existing tests - Create RunFunc() and RunFuncWithVersion() wrapper functions - RunFuncWithVersion() downloads and caches specific func CLI versions - Refactor existing tests to use RunFunc() instead of exec.Command - Cached binaries stored in bin/func-cli// --- test/e2e/func_deploy_test.go | 6 +- test/utils/func.go | 113 +++++++++++++++++++++++++++++++++++ test/utils/git.go | 5 +- 3 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 test/utils/func.go diff --git a/test/e2e/func_deploy_test.go b/test/e2e/func_deploy_test.go index 3e8630f..2a91931 100644 --- a/test/e2e/func_deploy_test.go +++ b/test/e2e/func_deploy_test.go @@ -63,19 +63,17 @@ var _ = Describe("Operator", func() { DeferCleanup(cleanupNamespaces, functionNamespace) // Deploy function using func CLI - cmd := exec.Command("func", "deploy", + out, err := utils.RunFunc("deploy", "--namespace", functionNamespace, "--path", repoDir, "--registry", registry, "--registry-insecure", strconv.FormatBool(registryInsecure)) - out, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred()) _, _ = fmt.Fprint(GinkgoWriter, out) // Cleanup func deployment DeferCleanup(func() { - cmd := exec.Command("func", "delete", "--path", repoDir, "--namespace", functionNamespace) - _, _ = utils.Run(cmd) + _, _ = utils.RunFunc("delete", "--path", repoDir, "--namespace", functionNamespace) }) // Commit func.yaml changes diff --git a/test/utils/func.go b/test/utils/func.go new file mode 100644 index 0000000..8fbaa95 --- /dev/null +++ b/test/utils/func.go @@ -0,0 +1,113 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +// RunFunc executes the func CLI with the current/latest version +func RunFunc(command string, args ...string) (string, error) { + allArgs := append([]string{command}, args...) + cmd := exec.Command("func", allArgs...) + return Run(cmd) +} + +// RunFuncWithVersion executes the func CLI with a specific version +// It downloads and caches the version if not already present +func RunFuncWithVersion(version string, command string, args ...string) (string, error) { + funcBinary, err := ensureFuncVersion(version) + if err != nil { + return "", err + } + + allArgs := append([]string{command}, args...) + cmd := exec.Command(funcBinary, allArgs...) + return Run(cmd) +} + +// ensureFuncVersion ensures the specified func version is available and returns its path +func ensureFuncVersion(version string) (string, error) { + projectDir, err := GetProjectDir() + if err != nil { + return "", fmt.Errorf("failed to get project directory: %w", err) + } + + versionDir := filepath.Join(projectDir, "bin", "func-cli", version) + funcBinary := filepath.Join(versionDir, "func") + + // Check if already cached + if _, err := os.Stat(funcBinary); err == nil { + return funcBinary, nil + } + + // Download the version + if err := downloadFuncVersion(version, versionDir, funcBinary); err != nil { + return "", err + } + + return funcBinary, nil +} + +// downloadFuncVersion downloads the specified func version from GitHub releases +func downloadFuncVersion(version, versionDir, funcBinary string) error { + // Create version directory + if err := os.MkdirAll(versionDir, 0755); err != nil { + return fmt.Errorf("failed to create version directory: %w", err) + } + + // Construct download URL + goos := runtime.GOOS + goarch := runtime.GOARCH + url := fmt.Sprintf("https://github.com/knative/func/releases/download/knative-%s/func_%s_%s", + version, goos, goarch) + + // Download binary + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("failed to download func %s: %w", version, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download func %s: HTTP %d", version, resp.StatusCode) + } + + // Write to file + out, err := os.Create(funcBinary) + if err != nil { + return fmt.Errorf("failed to create binary file: %w", err) + } + defer out.Close() + + if _, err := io.Copy(out, resp.Body); err != nil { + return fmt.Errorf("failed to write binary: %w", err) + } + + // Make executable + if err := os.Chmod(funcBinary, 0755); err != nil { + return fmt.Errorf("failed to make binary executable: %w", err) + } + + return nil +} diff --git a/test/utils/git.go b/test/utils/git.go index 3598fc5..5b16be9 100644 --- a/test/utils/git.go +++ b/test/utils/git.go @@ -39,13 +39,12 @@ func InitializeRepoWithFunction(repoURL, username, password, language string) (r authURL := buildAuthURL(repoURL, username, password) // Initialize function (func init creates the directory) - cmd := exec.Command("func", "init", "-l", language, repoDir) - if _, err = Run(cmd); err != nil { + if _, err = RunFunc("init", "-l", language, repoDir); err != nil { return "", fmt.Errorf("failed to init function: %w", err) } // Initialize git repo with main as default branch - cmd = exec.Command("git", "-C", repoDir, "init", "-b", "main") + cmd := exec.Command("git", "-C", repoDir, "init", "-b", "main") if _, err = Run(cmd); err != nil { return "", fmt.Errorf("failed to git init: %w", err) } From 70bc623c4cc1641094a2a043f727a80ab157bbaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 1 Apr 2026 13:30:29 +0200 Subject: [PATCH 2/9] Add e2e test for middleware update - Test deploys function with old func CLI (v1.20.0) - Operator should detect old middleware and update to latest - Verifies function becomes Ready after middleware update --- test/e2e/func_middleware_update_test.go | 156 ++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 test/e2e/func_middleware_update_test.go diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go new file mode 100644 index 0000000..d073e95 --- /dev/null +++ b/test/e2e/func_middleware_update_test.go @@ -0,0 +1,156 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "time" + + functionsdevv1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1" + "github.com/functions-dev/func-operator/test/utils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("Middleware Update", func() { + + SetDefaultEventuallyTimeout(2 * time.Minute) + SetDefaultEventuallyPollingInterval(time.Second) + + Context("with a function deployed using old func CLI", func() { + var repoURL string + var repoDir string + var functionName, functionNamespace string + + BeforeEach(func() { + var err error + + // Create repository provider resources with automatic cleanup + username, password, _, cleanup, err := repoProvider.CreateRandomUser() + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(cleanup) + + _, repoURL, cleanup, err = repoProvider.CreateRandomRepo(username, false) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(cleanup) + + // Initialize repository with function code + repoDir, err = utils.InitializeRepoWithFunction(repoURL, username, password, "go") + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(os.RemoveAll, repoDir) + + functionNamespace, err = utils.GetTestNamespace() + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(cleanupNamespaces, functionNamespace) + + // Deploy function using OLD func CLI version + out, err := utils.RunFuncWithVersion("v1.20.0", "deploy", + "--namespace", functionNamespace, + "--path", repoDir, + "--registry", registry, + "--registry-insecure", strconv.FormatBool(registryInsecure)) + Expect(err).NotTo(HaveOccurred()) + _, _ = fmt.Fprint(GinkgoWriter, out) + + // Cleanup func deployment + DeferCleanup(func() { + _, _ = utils.RunFunc("delete", "--path", repoDir, "--namespace", functionNamespace) + }) + + // Commit func.yaml changes + err = utils.CommitAndPush(repoDir, "Update func.yaml after deploy", "func.yaml") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + specReport := CurrentSpecReport() + if specReport.Failed() { + if functionName != "" { + cmd := exec.Command("kubectl", "get", "function", functionName, "-n", functionNamespace, "-o", "yaml") + function, err := utils.Run(cmd) + if err == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Function:\n %s", function) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get function: %s", err) + } + } + + By("Fetching controller manager pod logs") + cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace) + controllerLogs, err := utils.Run(cmd) + if err == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) + } + } + + // Cleanup function resource + if functionName != "" { + cmd := exec.Command("kubectl", "delete", "function", functionName, "-n", functionNamespace, "--ignore-not-found") + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + } + }) + + It("should update the middleware and mark the function as ready", func() { + // Create a Function resource + function := &functionsdevv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "my-function-", + Namespace: functionNamespace, + }, + Spec: functionsdevv1alpha1.FunctionSpec{ + Source: functionsdevv1alpha1.FunctionSpecSource{ + RepositoryURL: repoURL, + }, + Registry: functionsdevv1alpha1.FunctionSpecRegistry{ + Path: registry, + Insecure: registryInsecure, + }, + }, + } + + err := k8sClient.Create(ctx, function) + Expect(err).NotTo(HaveOccurred()) + + functionName = function.Name + + funcBecomeReady := func(g Gomega) { + fn := &functionsdevv1alpha1.Function{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: function.Name, Namespace: function.Namespace}, fn) + g.Expect(err).NotTo(HaveOccurred()) + + for _, cond := range fn.Status.Conditions { + if cond.Type == functionsdevv1alpha1.TypeReady { + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + return + } + } + g.Expect(false).To(BeTrue(), "Ready condition not found") + } + + // Middleware update could take a bit longer therefore give more time + Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed()) + }) + }) +}) From 279d70b9d10c1f140984b790791ac3b988d4a404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 1 Apr 2026 16:04:38 +0200 Subject: [PATCH 3/9] Update middleware e2e test to verify via CR condition - Can't reliably check middleware version via func describe with insecure registry - func describe requires inspecting OCI image labels which doesn't work with kind-registry:5000 - Instead verify middleware update by checking MiddlewareUpToDate condition transitions to True - Test now passes successfully --- test/e2e/func_middleware_update_test.go | 38 +++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index d073e95..885415a 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -113,8 +113,13 @@ var _ = Describe("Middleware Update", func() { }) It("should update the middleware and mark the function as ready", func() { + // Note: We can't reliably check middleware version via func describe in e2e tests + // because func describe requires inspecting OCI image labels, which doesn't work + // with insecure registries (kind-registry:5000). + // Instead, we'll verify the middleware update by checking the CR conditions. + // Create a Function resource - function := &functionsdevv1alpha1.Function{ + fn := &functionsdevv1alpha1.Function{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "my-function-", Namespace: functionNamespace, @@ -130,17 +135,17 @@ var _ = Describe("Middleware Update", func() { }, } - err := k8sClient.Create(ctx, function) + err := k8sClient.Create(ctx, fn) Expect(err).NotTo(HaveOccurred()) - functionName = function.Name + functionName = fn.Name funcBecomeReady := func(g Gomega) { - fn := &functionsdevv1alpha1.Function{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: function.Name, Namespace: function.Namespace}, fn) + f := &functionsdevv1alpha1.Function{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: fn.Name, Namespace: fn.Namespace}, f) g.Expect(err).NotTo(HaveOccurred()) - for _, cond := range fn.Status.Conditions { + for _, cond := range f.Status.Conditions { if cond.Type == functionsdevv1alpha1.TypeReady { g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) return @@ -151,6 +156,27 @@ var _ = Describe("Middleware Update", func() { // Middleware update could take a bit longer therefore give more time Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed()) + + // Verify middleware was updated by checking the MiddlewareUpToDate condition + updatedFunction := &functionsdevv1alpha1.Function{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: fn.Name, Namespace: fn.Namespace}, updatedFunction) + Expect(err).NotTo(HaveOccurred()) + + // Check that MiddlewareUpToDate condition is now True + var middlewareUpToDate bool + var middlewareCondition metav1.Condition + for _, cond := range updatedFunction.Status.Conditions { + if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate { + middlewareUpToDate = cond.Status == metav1.ConditionTrue + middlewareCondition = cond + break + } + } + _, _ = fmt.Fprintf(GinkgoWriter, "MiddlewareUpToDate condition: status=%s, reason=%s\n", + middlewareCondition.Status, middlewareCondition.Reason) + + Expect(middlewareUpToDate).To(BeTrue(), + "MiddlewareUpToDate condition should be True after operator redeploys") }) }) }) From cdb859b624da47a11b4983fee2e4e23a782bd33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 1 Apr 2026 17:36:52 +0200 Subject: [PATCH 4/9] Add end-to-end verification of middleware update via skopeo - Use skopeo to inspect OCI image labels and verify middleware-version - Initial image (v1.20.0 deploy) has no middleware-version label - Updated image (operator redeploy) has middleware-version set - Handle image references with both tag and digest (remove tag for skopeo) - Test now fully verifies the core operator feature end-to-end Why skopeo instead of func describe: - func describe uses go-containerregistry to read middleware-version from image - kind-registry is a Docker container name, not a real hostname - Docker CLI can resolve it (func deploy works), but go-containerregistry cannot - func describe silently fails to populate middleware.version field - skopeo with localhost:5001 successfully inspects the image labels --- test/e2e/func_middleware_update_test.go | 136 +++++++++++++++++++++--- 1 file changed, 122 insertions(+), 14 deletions(-) diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index 885415a..94ba66b 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -17,18 +17,23 @@ limitations under the License. package e2e import ( + "encoding/json" "fmt" "os" "os/exec" "strconv" + "strings" "time" functionsdevv1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1" + "github.com/functions-dev/func-operator/internal/function" "github.com/functions-dev/func-operator/test/utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + funcfn "knative.dev/func/pkg/functions" ) var _ = Describe("Middleware Update", func() { @@ -113,10 +118,70 @@ var _ = Describe("Middleware Update", func() { }) It("should update the middleware and mark the function as ready", func() { - // Note: We can't reliably check middleware version via func describe in e2e tests - // because func describe requires inspecting OCI image labels, which doesn't work - // with insecure registries (kind-registry:5000). - // Instead, we'll verify the middleware update by checking the CR conditions. + // Get function metadata to retrieve the deployed function name + funcMetadata, err := function.Metadata(repoDir) + Expect(err).NotTo(HaveOccurred()) + deployedFunctionName := funcMetadata.Name + + // NOTE: We use skopeo to verify the middleware version because func describe + // cannot access the image registry in our test environment. + // + // func describe calls MiddlewareVersion() which uses go-containerregistry's + // remote.Get() to fetch the image manifest and read the middleware-version label. + // However, the image reference is kind-registry:5000/..., where "kind-registry" + // is a Docker container name, not a real hostname. + // + // Docker CLI can resolve container names on Docker networks (which is why + // func deploy --registry kind-registry:5000 works for pushing images), but + // go-containerregistry is not Docker-aware and tries regular DNS resolution, + // which fails. The error is silently ignored in describers. + // + // We use skopeo with localhost:5001 (port-forward to the registry) to + // directly inspect the OCI image labels and verify the middleware was updated. + + // Get initial image digest from func describe (deployed with old v1.20.0) + out, err := utils.RunFunc("describe", deployedFunctionName, "-n", functionNamespace, "-o", "yaml") + Expect(err).NotTo(HaveOccurred()) + + var initialInstance funcfn.Instance + err = yaml.Unmarshal([]byte(out), &initialInstance) + Expect(err).NotTo(HaveOccurred()) + + initialImage := initialInstance.Image + Expect(initialImage).NotTo(BeEmpty(), "Initial image should be available from func describe") + _, _ = fmt.Fprintf(GinkgoWriter, "Initial image (deployed with v1.20.0): %s\n", initialImage) + + // Verify initial image has no middleware-version label (old func CLI) + initialImageLocal := strings.Replace(initialImage, "kind-registry:5000", "localhost:5001", 1) + // Remove tag if both tag and digest are present (skopeo doesn't support this format) + if strings.Contains(initialImageLocal, "@") { + atIndex := strings.Index(initialImageLocal, "@") + slashIndex := strings.LastIndex(initialImageLocal[:atIndex], "/") + if slashIndex != -1 { + betweenSlashAndAt := initialImageLocal[slashIndex+1 : atIndex] + if strings.Contains(betweenSlashAndAt, ":") { + colonIndex := strings.Index(betweenSlashAndAt, ":") + initialImageLocal = initialImageLocal[:slashIndex+1+colonIndex] + initialImageLocal[atIndex:] + } + } + } + cmd := exec.Command("skopeo", + "inspect", + "--tls-verify=false", + "--no-tags", + "docker://"+initialImageLocal) + skopeoOutput, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + var initialImageLabels struct { + Labels map[string]string `json:"Labels"` + } + err = json.Unmarshal([]byte(skopeoOutput), &initialImageLabels) + Expect(err).NotTo(HaveOccurred()) + + initialMiddlewareVersion := initialImageLabels.Labels["middleware-version"] + _, _ = fmt.Fprintf(GinkgoWriter, "Initial middleware-version label: '%s' (expected empty for v1.20.0)\n", + initialMiddlewareVersion) // Create a Function resource fn := &functionsdevv1alpha1.Function{ @@ -135,7 +200,7 @@ var _ = Describe("Middleware Update", func() { }, } - err := k8sClient.Create(ctx, fn) + err = k8sClient.Create(ctx, fn) Expect(err).NotTo(HaveOccurred()) functionName = fn.Name @@ -157,26 +222,69 @@ var _ = Describe("Middleware Update", func() { // Middleware update could take a bit longer therefore give more time Eventually(funcBecomeReady, 6*time.Minute).Should(Succeed()) - // Verify middleware was updated by checking the MiddlewareUpToDate condition + // Verify middleware was actually updated by inspecting the new image + out, err = utils.RunFunc("describe", deployedFunctionName, "-n", functionNamespace, "-o", "yaml") + Expect(err).NotTo(HaveOccurred()) + + var updatedInstance funcfn.Instance + err = yaml.Unmarshal([]byte(out), &updatedInstance) + Expect(err).NotTo(HaveOccurred()) + + updatedImage := updatedInstance.Image + Expect(updatedImage).NotTo(BeEmpty(), "Updated image should be available from func describe") + _, _ = fmt.Fprintf(GinkgoWriter, "Updated image (redeployed by operator): %s\n", updatedImage) + + // Verify the image actually changed + Expect(updatedImage).NotTo(Equal(initialImage), "Image should have changed after operator redeploy") + + // Verify updated image has middleware-version label set + updatedImageLocal := strings.Replace(updatedImage, "kind-registry:5000", "localhost:5001", 1) + // Remove tag if both tag and digest are present (skopeo doesn't support this format) + // Format: registry/name:tag@digest -> registry/name@digest + if strings.Contains(updatedImageLocal, "@") { + atIndex := strings.Index(updatedImageLocal, "@") + slashIndex := strings.LastIndex(updatedImageLocal[:atIndex], "/") + if slashIndex != -1 { + // Check if there's a colon between last slash and @ + betweenSlashAndAt := updatedImageLocal[slashIndex+1 : atIndex] + if strings.Contains(betweenSlashAndAt, ":") { + // Remove the :tag part + colonIndex := strings.Index(betweenSlashAndAt, ":") + updatedImageLocal = updatedImageLocal[:slashIndex+1+colonIndex] + updatedImageLocal[atIndex:] + } + } + } + cmd = exec.Command("skopeo", "inspect", "--tls-verify=false", "--no-tags", "docker://"+updatedImageLocal) + skopeoOutput, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + var updatedImageLabels struct { + Labels map[string]string `json:"Labels"` + } + err = json.Unmarshal([]byte(skopeoOutput), &updatedImageLabels) + Expect(err).NotTo(HaveOccurred()) + + updatedMiddlewareVersion := updatedImageLabels.Labels["middleware-version"] + _, _ = fmt.Fprintf(GinkgoWriter, "Updated middleware-version label: '%s'\n", updatedMiddlewareVersion) + + // The operator should have set a middleware version + Expect(updatedMiddlewareVersion).NotTo(BeEmpty(), "Operator should have deployed with middleware-version label set") + + // Verify MiddlewareUpToDate condition is True updatedFunction := &functionsdevv1alpha1.Function{} err = k8sClient.Get(ctx, types.NamespacedName{Name: fn.Name, Namespace: fn.Namespace}, updatedFunction) Expect(err).NotTo(HaveOccurred()) - // Check that MiddlewareUpToDate condition is now True var middlewareUpToDate bool - var middlewareCondition metav1.Condition for _, cond := range updatedFunction.Status.Conditions { if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate { middlewareUpToDate = cond.Status == metav1.ConditionTrue - middlewareCondition = cond + _, _ = fmt.Fprintf(GinkgoWriter, "MiddlewareUpToDate condition: status=%s, reason=%s\n", + cond.Status, cond.Reason) break } } - _, _ = fmt.Fprintf(GinkgoWriter, "MiddlewareUpToDate condition: status=%s, reason=%s\n", - middlewareCondition.Status, middlewareCondition.Reason) - - Expect(middlewareUpToDate).To(BeTrue(), - "MiddlewareUpToDate condition should be True after operator redeploys") + Expect(middlewareUpToDate).To(BeTrue(), "MiddlewareUpToDate condition should be True") }) }) }) From 1d11db6c99d3342f5d2e6459f747f94c02fa2d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 1 Apr 2026 17:48:44 +0200 Subject: [PATCH 5/9] Fix linter: extract duplicated AfterEach code into helper - Add logFailedTestDetails() helper in e2e_test.go - Replace duplicated AfterEach blocks in func_deploy_test.go and func_middleware_update_test.go - Eliminates dupl linter warnings --- test/e2e/e2e_test.go | 27 +++++++++++++++++++++++++ test/e2e/func_deploy_test.go | 22 +------------------- test/e2e/func_middleware_update_test.go | 22 +------------------- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index bcd5ba3..1426607 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -217,3 +217,30 @@ func getMetricsOutput() string { Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) return metricsOutput } + +// logFailedTestDetails logs function resource and controller logs on test failure +func logFailedTestDetails(functionName, functionNamespace string) { + specReport := CurrentSpecReport() + if !specReport.Failed() { + return + } + + if functionName != "" { + cmd := exec.Command("kubectl", "get", "function", functionName, "-n", functionNamespace, "-o", "yaml") + function, err := utils.Run(cmd) + if err == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Function:\n %s", function) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get function: %s", err) + } + } + + By("Fetching controller manager pod logs") + cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace) + controllerLogs, err := utils.Run(cmd) + if err == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) + } +} diff --git a/test/e2e/func_deploy_test.go b/test/e2e/func_deploy_test.go index 2a91931..f78d8de 100644 --- a/test/e2e/func_deploy_test.go +++ b/test/e2e/func_deploy_test.go @@ -82,27 +82,7 @@ var _ = Describe("Operator", func() { }) AfterEach(func() { - specReport := CurrentSpecReport() - if specReport.Failed() { - if functionName != "" { - cmd := exec.Command("kubectl", "get", "function", functionName, "-n", functionNamespace, "-o", "yaml") - function, err := utils.Run(cmd) - if err == nil { - _, _ = fmt.Fprintf(GinkgoWriter, "Function:\n %s", function) - } else { - _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get function: %s", err) - } - } - - By("Fetching controller manager pod logs") - cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace) - controllerLogs, err := utils.Run(cmd) - if err == nil { - _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) - } else { - _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) - } - } + logFailedTestDetails(functionName, functionNamespace) // Cleanup function resource if functionName != "" { diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index 94ba66b..7c055d3 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -87,27 +87,7 @@ var _ = Describe("Middleware Update", func() { }) AfterEach(func() { - specReport := CurrentSpecReport() - if specReport.Failed() { - if functionName != "" { - cmd := exec.Command("kubectl", "get", "function", functionName, "-n", functionNamespace, "-o", "yaml") - function, err := utils.Run(cmd) - if err == nil { - _, _ = fmt.Fprintf(GinkgoWriter, "Function:\n %s", function) - } else { - _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get function: %s", err) - } - } - - By("Fetching controller manager pod logs") - cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace) - controllerLogs, err := utils.Run(cmd) - if err == nil { - _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) - } else { - _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) - } - } + logFailedTestDetails(functionName, functionNamespace) // Cleanup function resource if functionName != "" { From dac7dee5191316c443cd0861a3a52b6b49710993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 1 Apr 2026 18:39:02 +0200 Subject: [PATCH 6/9] Disable HA setup for Gitea (#31) --- hack/create-kind-cluster.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hack/create-kind-cluster.sh b/hack/create-kind-cluster.sh index 9920c2e..7ade76f 100755 --- a/hack/create-kind-cluster.sh +++ b/hack/create-kind-cluster.sh @@ -177,7 +177,12 @@ function install_gitea() { --set gitea.admin.username=giteaadmin \ --set gitea.admin.password=giteapass \ --set gitea.admin.email=admin@gitea.local \ - --set persistence.enabled=false + --set persistence.enabled=false \ + --set postgresql-ha.enabled=false \ + --set postgresql.enabled=true \ + --set postgresql.persistence.enabled=false \ + --set redis-cluster.enabled=false \ + --set redis.enabled=false header_text "Waiting for Gitea to become ready" kubectl wait deployment --all --timeout=-1s --for=condition=Available --namespace gitea From 769c4d1bb149e584a77e0b2a9ced83defae87117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Wed, 1 Apr 2026 22:56:41 +0200 Subject: [PATCH 7/9] Use v1.20.1 for both init and deploy in middleware update test The middleware update test was failing because: - InitializeRepoWithFunction() used current func CLI to init the repo - Current func CLI creates new instance-based template structure - Old func CLI (v1.20.0) couldn't understand the new template format - This caused buildpack lifecycle failures (status code 51) Solution: - Add InitializeRepoWithFunctionVersion() to allow specifying func CLI version - Use v1.20.1 for both 'func init' and 'func deploy' to ensure compatibility - v1.20.1 still doesn't have middleware-version label (added in v1.21.0), so it's suitable for testing the middleware update functionality --- .github/workflows/test-e2e-bundle.yml | 4 ++++ .github/workflows/test-e2e.yml | 4 ++++ test/e2e/func_middleware_update_test.go | 24 +++++++++++++----------- test/utils/git.go | 13 ++++++++++++- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-e2e-bundle.yml b/.github/workflows/test-e2e-bundle.yml index 62a387a..b6ea26c 100644 --- a/.github/workflows/test-e2e-bundle.yml +++ b/.github/workflows/test-e2e-bundle.yml @@ -4,6 +4,10 @@ on: push: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test-bundle-e2e: name: Bundle E2E Tests diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index c8ef62e..d106086 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -4,6 +4,10 @@ on: push: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test-e2e: name: E2E Tests diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index 7c055d3..9afef0c 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -58,17 +58,19 @@ var _ = Describe("Middleware Update", func() { Expect(err).NotTo(HaveOccurred()) DeferCleanup(cleanup) - // Initialize repository with function code - repoDir, err = utils.InitializeRepoWithFunction(repoURL, username, password, "go") - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(os.RemoveAll, repoDir) - functionNamespace, err = utils.GetTestNamespace() Expect(err).NotTo(HaveOccurred()) DeferCleanup(cleanupNamespaces, functionNamespace) - // Deploy function using OLD func CLI version - out, err := utils.RunFuncWithVersion("v1.20.0", "deploy", + // Initialize repository with function code using OLD func CLI version + // v1.20.1 has no middleware-version label and uses instance-compatible templates + oldFuncVersion := "v1.20.1" + repoDir, err = utils.InitializeRepoWithFunctionVersion(repoURL, username, password, "go", oldFuncVersion) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(os.RemoveAll, repoDir) + + // Deploy function using the same OLD func CLI version + out, err := utils.RunFuncWithVersion(oldFuncVersion, "deploy", "--namespace", functionNamespace, "--path", repoDir, "--registry", registry, @@ -119,7 +121,7 @@ var _ = Describe("Middleware Update", func() { // We use skopeo with localhost:5001 (port-forward to the registry) to // directly inspect the OCI image labels and verify the middleware was updated. - // Get initial image digest from func describe (deployed with old v1.20.0) + // Get initial image digest from func describe (deployed with v1.20.1) out, err := utils.RunFunc("describe", deployedFunctionName, "-n", functionNamespace, "-o", "yaml") Expect(err).NotTo(HaveOccurred()) @@ -129,9 +131,9 @@ var _ = Describe("Middleware Update", func() { initialImage := initialInstance.Image Expect(initialImage).NotTo(BeEmpty(), "Initial image should be available from func describe") - _, _ = fmt.Fprintf(GinkgoWriter, "Initial image (deployed with v1.20.0): %s\n", initialImage) + _, _ = fmt.Fprintf(GinkgoWriter, "Initial image (deployed with v1.20.1): %s\n", initialImage) - // Verify initial image has no middleware-version label (old func CLI) + // Verify initial image has no middleware-version label (v1.20.1 doesn't set it) initialImageLocal := strings.Replace(initialImage, "kind-registry:5000", "localhost:5001", 1) // Remove tag if both tag and digest are present (skopeo doesn't support this format) if strings.Contains(initialImageLocal, "@") { @@ -160,7 +162,7 @@ var _ = Describe("Middleware Update", func() { Expect(err).NotTo(HaveOccurred()) initialMiddlewareVersion := initialImageLabels.Labels["middleware-version"] - _, _ = fmt.Fprintf(GinkgoWriter, "Initial middleware-version label: '%s' (expected empty for v1.20.0)\n", + _, _ = fmt.Fprintf(GinkgoWriter, "Initial middleware-version label: '%s' (expected empty for v1.20.1)\n", initialMiddlewareVersion) // Create a Function resource diff --git a/test/utils/git.go b/test/utils/git.go index 5b16be9..b9cc4c0 100644 --- a/test/utils/git.go +++ b/test/utils/git.go @@ -33,13 +33,24 @@ func buildAuthURL(repoURL, username, password string) string { // InitializeRepoWithFunction creates a function project and pushes it to the Gitea repo func InitializeRepoWithFunction(repoURL, username, password, language string) (repoDir string, err error) { + return InitializeRepoWithFunctionVersion(repoURL, username, password, language, "") +} + +// InitializeRepoWithFunctionVersion creates a function project with a specific func CLI version +// If version is empty, uses the current func CLI +func InitializeRepoWithFunctionVersion(repoURL, username, password, language, version string) (repoDir string, err error) { repoDir = fmt.Sprintf("%s/func-test-%s", os.TempDir(), rand.String(10)) // Build authenticated URL authURL := buildAuthURL(repoURL, username, password) // Initialize function (func init creates the directory) - if _, err = RunFunc("init", "-l", language, repoDir); err != nil { + if version == "" { + _, err = RunFunc("init", "-l", language, repoDir) + } else { + _, err = RunFuncWithVersion(version, "init", "-l", language, repoDir) + } + if err != nil { return "", fmt.Errorf("failed to init function: %w", err) } From 0c565478cef5eb3a9b1354e8186e323ce4fbb3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Thu, 2 Apr 2026 11:08:02 +0200 Subject: [PATCH 8/9] Fix linter issue --- test/utils/git.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/test/utils/git.go b/test/utils/git.go index b9cc4c0..445041f 100644 --- a/test/utils/git.go +++ b/test/utils/git.go @@ -32,61 +32,62 @@ func buildAuthURL(repoURL, username, password string) string { } // InitializeRepoWithFunction creates a function project and pushes it to the Gitea repo -func InitializeRepoWithFunction(repoURL, username, password, language string) (repoDir string, err error) { +func InitializeRepoWithFunction(repoURL, username, password, language string) (string, error) { return InitializeRepoWithFunctionVersion(repoURL, username, password, language, "") } // InitializeRepoWithFunctionVersion creates a function project with a specific func CLI version // If version is empty, uses the current func CLI -func InitializeRepoWithFunctionVersion(repoURL, username, password, language, version string) (repoDir string, err error) { - repoDir = fmt.Sprintf("%s/func-test-%s", os.TempDir(), rand.String(10)) +func InitializeRepoWithFunctionVersion(repoURL, username, password, language, version string) (string, error) { + repoDir := fmt.Sprintf("%s/func-test-%s", os.TempDir(), rand.String(10)) // Build authenticated URL authURL := buildAuthURL(repoURL, username, password) // Initialize function (func init creates the directory) if version == "" { - _, err = RunFunc("init", "-l", language, repoDir) + if _, err := RunFunc("init", "-l", language, repoDir); err != nil { + return "", fmt.Errorf("failed to init function: %w", err) + } } else { - _, err = RunFuncWithVersion(version, "init", "-l", language, repoDir) - } - if err != nil { - return "", fmt.Errorf("failed to init function: %w", err) + if _, err := RunFuncWithVersion(version, "init", "-l", language, repoDir); err != nil { + return "", fmt.Errorf("failed to init function: %w", err) + } } // Initialize git repo with main as default branch cmd := exec.Command("git", "-C", repoDir, "init", "-b", "main") - if _, err = Run(cmd); err != nil { + if _, err := Run(cmd); err != nil { return "", fmt.Errorf("failed to git init: %w", err) } // Configure git user cmd = exec.Command("git", "-C", repoDir, "config", "user.name", "Test User") - if _, err = Run(cmd); err != nil { + if _, err := Run(cmd); err != nil { return "", fmt.Errorf("failed to set git user.name: %w", err) } cmd = exec.Command("git", "-C", repoDir, "config", "user.email", "test@example.com") - if _, err = Run(cmd); err != nil { + if _, err := Run(cmd); err != nil { return "", fmt.Errorf("failed to set git user.email: %w", err) } // Add remote cmd = exec.Command("git", "-C", repoDir, "remote", "add", "origin", authURL) - if _, err = Run(cmd); err != nil { + if _, err := Run(cmd); err != nil { return "", fmt.Errorf("failed to add remote: %w", err) } // Commit and push cmd = exec.Command("git", "-C", repoDir, "add", ".") - if _, err = Run(cmd); err != nil { + if _, err := Run(cmd); err != nil { return "", fmt.Errorf("failed to git add: %w", err) } cmd = exec.Command("git", "-C", repoDir, "commit", "-m", "Initial function") - if _, err = Run(cmd); err != nil { + if _, err := Run(cmd); err != nil { return "", fmt.Errorf("failed to git commit: %w", err) } cmd = exec.Command("git", "-C", repoDir, "push", "-u", "origin", "main") - if _, err = Run(cmd); err != nil { + if _, err := Run(cmd); err != nil { return "", fmt.Errorf("failed to push initial commit: %w", err) } From 67b6e98778971af51265caa9adc6bf202a123d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Thu, 2 Apr 2026 11:41:28 +0200 Subject: [PATCH 9/9] Add -timeout 1h to go test command in test-e2e target The test-e2e target had -ginkgo.timeout=1h but was missing the -timeout flag for go test itself, which defaults to 10 minutes. This caused tests to timeout during namespace cleanup even though the tests themselves passed successfully. Aligns with test-e2e-bundle target which already has -timeout 1h. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c4fa061..7d3e945 100644 --- a/Makefile +++ b/Makefile @@ -120,7 +120,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests. .PHONY: test-e2e ## Run e2e tests. test-e2e: - go test ./test/e2e/ -v -ginkgo.v -ginkgo.timeout=1h -ginkgo.label-filter="!bundle" + go test -timeout 1h ./test/e2e/ -v -ginkgo.v -ginkgo.timeout=1h -ginkgo.label-filter="!bundle" .PHONY: test-e2e-bundle ## Run bundle e2e tests. test-e2e-bundle: operator-sdk docker-build docker-push bundle bundle-build bundle-push install-olm-in-cluster