From eb1db45b61883f6a28f2d3fe120bbe21b6c26159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Mon, 13 Apr 2026 12:21:41 +0200 Subject: [PATCH 01/10] Add possibility to specify operator default for middleware update enablement --- api/v1alpha1/function_lifecycle.go | 10 ++ api/v1alpha1/function_types.go | 1 - cmd/main.go | 17 +++- config/manager/manager.yaml | 17 ++++ internal/controller/function_controller.go | 57 ++++++++++- .../controller/function_controller_test.go | 95 +++++++++++++++++-- 6 files changed, 180 insertions(+), 17 deletions(-) diff --git a/api/v1alpha1/function_lifecycle.go b/api/v1alpha1/function_lifecycle.go index 3cc27e5..d782211 100644 --- a/api/v1alpha1/function_lifecycle.go +++ b/api/v1alpha1/function_lifecycle.go @@ -184,3 +184,13 @@ func (f *Function) MarkMiddlewareNotUpToDate(reason, messageFormat string, messa ObservedGeneration: f.Generation, }) } + +func (f *Function) MarkMiddlewareNotUpToDateIntentionally(reason, messageFormat string, messageA ...interface{}) bool { + return meta.SetStatusCondition(&f.Status.Conditions, metav1.Condition{ + Type: TypeMiddlewareUpToDate, + Status: metav1.ConditionTrue, + Reason: reason, + Message: fmt.Sprintf(messageFormat, messageA...), + ObservedGeneration: f.Generation, + }) +} diff --git a/api/v1alpha1/function_types.go b/api/v1alpha1/function_types.go index 0e84b6c..61df2f2 100644 --- a/api/v1alpha1/function_types.go +++ b/api/v1alpha1/function_types.go @@ -43,7 +43,6 @@ type FunctionSpec struct { // AutoUpdateMiddleware defines if the operator should rebuild the function when an outdated middleware is detected. // Defaults to the global operator config. - // TODO: implement logic AutoUpdateMiddleware *bool `json:"autoUpdateMiddleware,omitempty"` } diff --git a/cmd/main.go b/cmd/main.go index 337e942..f1aac53 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -251,12 +251,19 @@ func main() { } setupLog.Info("Func CLI is ready") + operatorNamespace := os.Getenv("SYSTEM_NAMESPACE") + if operatorNamespace == "" { + setupLog.Info("Operator namespace not set, defaulting to func-operator-system") + operatorNamespace = "func-operator-system" + } + if err := (&controller.FunctionReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorder("functions-controller"), - FuncCliManager: funcCLIManager, - GitManager: git.NewManager(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorder("functions-controller"), + FuncCliManager: funcCLIManager, + GitManager: git.NewManager(), + OperatorNamespace: operatorNamespace, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Function") os.Exit(1) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index d3bf9c6..f35cbcb 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -7,6 +7,19 @@ metadata: app.kubernetes.io/managed-by: kustomize name: system --- +apiVersion: v1 +kind: ConfigMap +metadata: + name: controller-config + namespace: system + labels: + control-plane: controller-manager + app.kubernetes.io/name: func-operator + app.kubernetes.io/managed-by: kustomize +data: + # default to enable middleware updates + autoUpdateMiddleware: "true" +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -70,6 +83,10 @@ spec: valueFrom: fieldRef: fieldPath: metadata.annotations['olm.targetNamespaces'] + - name: SYSTEM_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace image: controller:latest imagePullPolicy: Always name: manager diff --git a/internal/controller/function_controller.go b/internal/controller/function_controller.go index ecf9f55..2df31a7 100644 --- a/internal/controller/function_controller.go +++ b/internal/controller/function_controller.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "os" + "strconv" "strings" "github.com/functions-dev/func-operator/internal/funccli" @@ -46,15 +47,17 @@ import ( const ( deployFunctionRoleName = "func-operator-deploy-function" + controllerConfigName = "controller-config" ) // FunctionReconciler reconciles a Function object type FunctionReconciler struct { client.Client - Scheme *runtime.Scheme - Recorder events.EventRecorder - FuncCliManager funccli.Manager - GitManager git.Manager + Scheme *runtime.Scheme + Recorder events.EventRecorder + FuncCliManager funccli.Manager + GitManager git.Manager + OperatorNamespace string } // +kubebuilder:rbac:groups=functions.dev,resources=functions,verbs=get;list;watch;create;update;patch;delete @@ -214,7 +217,19 @@ func (r *FunctionReconciler) handleMiddlewareUpdate(ctx context.Context, functio } if !isOnLatestMiddleware { - logger.Info("Function is not on latest middleware. Will redeploy") + isMiddlewareUpdateEnabled, source, err := r.isMiddlewareUpdateEnabled(ctx, function) + if err != nil { + function.MarkMiddlewareNotUpToDate("MiddlewareCheckFailed", "Failed to check if middleware should be updated: %s", err) + return fmt.Errorf("failed to check if middleware should be updated: %w", err) + } + + if !isMiddlewareUpdateEnabled { + logger.Info("Skipping middleware update, as middleware update is disabled") + function.MarkMiddlewareNotUpToDateIntentionally("SkipMiddlewareUpdate", "Skipping middleware update as update is disabled (source: %s)", source) + return nil + } + + logger.Info("Function is not on latest middleware and middleware update is enabled. Will redeploy") function.MarkMiddlewareNotUpToDate("MiddlewareOutdated", "Middleware is outdated, redeploying") // update function image in status before long redeploy operation @@ -491,3 +506,35 @@ func (r *FunctionReconciler) isMiddlewareLatest(ctx context.Context, metadata *f return latestMiddleware == functionMiddleware, nil } + +// isMiddlewareUpdateEnabled returns if the middleware should be updated given by the functions spec or the operators +// default. +func (r *FunctionReconciler) isMiddlewareUpdateEnabled(ctx context.Context, function *v1alpha1.Function) (bool, string, error) { + logger := log.FromContext(ctx) + + // setting from function overrides operator default + if function.Spec.AutoUpdateMiddleware != nil { + return *function.Spec.AutoUpdateMiddleware, "function", nil + } + + // nothing defined in function spec --> check operator config + cm := &v1.ConfigMap{} + err := r.Get(ctx, types.NamespacedName{Namespace: r.OperatorNamespace, Name: controllerConfigName}, cm) + if err != nil { + return false, "", fmt.Errorf("failed to get operator config configmap: %w", err) + } + + val, ok := cm.Data["autoUpdateMiddleware"] + if !ok { + logger.Info("No autoUpdateMiddleware field in configmap found. Fallback to hardcoded autoUpdateMiddleware=true") + // TODO: check if returning an error would be better here + return true, "operator", nil + } + + boolVal, err := strconv.ParseBool(val) + if err != nil { + return false, "", fmt.Errorf("failed to parse autoUpdateMiddleware value from configmap: %w", err) + } + + return boolVal, "operator", nil +} diff --git a/internal/controller/function_controller_test.go b/internal/controller/function_controller_test.go index a7c6fe0..2c0115a 100644 --- a/internal/controller/function_controller_test.go +++ b/internal/controller/function_controller_test.go @@ -18,6 +18,7 @@ package controller import ( "context" + "fmt" "os" "path/filepath" @@ -27,10 +28,13 @@ import ( . "github.com/onsi/gomega" "github.com/stretchr/testify/mock" "gopkg.in/yaml.v3" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" "k8s.io/client-go/tools/events" + "k8s.io/utils/ptr" "knative.dev/func/pkg/functions" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -76,6 +80,7 @@ var _ = Describe("Function Controller", func() { spec functionsdevv1alpha1.FunctionSpec configureMocks func(*funccli.MockManager, *git.MockManager) statusChecks func(*functionsdevv1alpha1.FunctionStatus) + operatorConfig map[string]string } DescribeTable("should successfully reconcile the resource", @@ -89,13 +94,24 @@ var _ = Describe("Function Controller", func() { gitManagerMock := git.NewMockManager(GinkgoT()) tc.configureMocks(funcCliManagerMock, gitManagerMock) + operatorNamespace := fmt.Sprintf("func-operator-%s", rand.String(6)) + + By("Setting up the operator namespace") + err = createNamespace(operatorNamespace) + Expect(err).NotTo(HaveOccurred()) + + By("Setting up the controller config") + err = createControllerConfig(operatorNamespace, tc.operatorConfig) + Expect(err).NotTo(HaveOccurred()) + By("Reconciling the created resource") controllerReconciler := &FunctionReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - Recorder: &events.FakeRecorder{}, - FuncCliManager: funcCliManagerMock, - GitManager: gitManagerMock, + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Recorder: &events.FakeRecorder{}, + FuncCliManager: funcCliManagerMock, + GitManager: gitManagerMock, + OperatorNamespace: operatorNamespace, } _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ @@ -182,7 +198,6 @@ var _ = Describe("Function Controller", func() { Expect(status.Git.ObservedCommit).Should(Equal("foobar")) }, }), - Entry("should contain the deployment information in the status", reconcileTestCase{ spec: functionsdevv1alpha1.FunctionSpec{ Repository: functionsdevv1alpha1.FunctionSpecRepository{ @@ -212,10 +227,78 @@ var _ = Describe("Function Controller", func() { Expect(status.Deployment.Runtime).Should(Equal("node")) }, }), + Entry("should skip middleware update, when config is disabled", reconcileTestCase{ + spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: "https://github.com/foo/bar", + }, + AutoUpdateMiddleware: nil, + }, + configureMocks: func(funcMock *funccli.MockManager, gitMock *git.MockManager) { + funcMock.EXPECT().Describe(mock.Anything, functionName, resourceNamespace).Return(functions.Instance{ + Middleware: functions.Middleware{ + Version: "v1.0.0", + }, + Image: "my-image:v1.2.3", + }, nil) + funcMock.EXPECT().GetLatestMiddlewareVersion(mock.Anything, mock.Anything, mock.Anything).Return("v2.0.0", nil) + funcMock.EXPECT().GetMiddlewareVersion(mock.Anything, functionName, resourceNamespace).Return("v1.0.0", nil) + // no funcMock.EXPECT().Deploy call, as no redeploy expected! + + gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "", "main", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil) + }, + operatorConfig: map[string]string{ + "autoUpdateMiddleware": "false", + }, + }), + Entry("AutoUpdateMiddleware setting in function should take priority over operator config", reconcileTestCase{ + spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: "https://github.com/foo/bar", + }, + AutoUpdateMiddleware: ptr.To(false), + }, + configureMocks: func(funcMock *funccli.MockManager, gitMock *git.MockManager) { + funcMock.EXPECT().Describe(mock.Anything, functionName, resourceNamespace).Return(functions.Instance{ + Middleware: functions.Middleware{ + Version: "v1.0.0", + }, + Image: "my-image:v1.2.3", + }, nil) + funcMock.EXPECT().GetLatestMiddlewareVersion(mock.Anything, mock.Anything, mock.Anything).Return("v2.0.0", nil) + funcMock.EXPECT().GetMiddlewareVersion(mock.Anything, functionName, resourceNamespace).Return("v1.0.0", nil) + // no funcMock.EXPECT().Deploy call, as no redeploy expected! + + gitMock.EXPECT().CloneRepository(mock.Anything, "https://github.com/foo/bar", "", "main", mock.Anything).Return(createTmpGitRepo(functions.Function{Name: "func-go"}), nil) + }, + operatorConfig: map[string]string{ + "autoUpdateMiddleware": "true", + }, + }), ) }) }) +func createControllerConfig(operatorNamespace string, config map[string]string) error { + cm := v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerConfigName, + Namespace: operatorNamespace, + }, + Data: config, + } + + return k8sClient.Create(ctx, &cm) +} + +func createNamespace(ns string) error { + return k8sClient.Create(ctx, &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: ns, + }, + }) +} + func createFunctionResource(name, namespace string, spec functionsdevv1alpha1.FunctionSpec) error { resource := functionsdevv1alpha1.Function{ ObjectMeta: metav1.ObjectMeta{ From 9a8fe2cde19629b105520d6549d93c847dbb04f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Mon, 13 Apr 2026 12:28:10 +0200 Subject: [PATCH 02/10] Update readme --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 89f1867..c8536f2 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,42 @@ spec: The operator will clone the repository and use the specified path as the function root directory. +### Configuring Automatic Middleware Updates + +The operators main responsibility it to rebuild functions when outdated middleware is detected. Anyhow this behavior can be enabled/disabled at two levels: + +#### Operator-Level Default + +Configure the operator-wide default by editing the `func-operator-controller-config` ConfigMap in the operators namespace (`func-operator-system` by default): + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: func-operator-controller-config + namespace: func-operator-system +data: + autoUpdateMiddleware: "true" # or "false" to disable by default +``` + +#### Per-Function Override + +Individual functions can override the operator default using the `autoUpdateMiddleware` field: + +```yaml +apiVersion: functions.dev/v1alpha1 +kind: Function +metadata: + name: my-function + namespace: default +spec: + repository: + url: https://github.com/your-org/your-function.git + autoUpdateMiddleware: false # Disable middleware updates for this function +``` + +**Precedence:** Function-level settings always take priority over the operator default. + ## Development ### Local Development Cluster @@ -243,7 +279,7 @@ make lint | `repository.path` | string | No | Path to the function inside the repository. Defaults to "." | | `repository.authSecretRef` | object | No | Reference to the auth secret for private repository authentication | | `registry.authSecretRef` | object | No | Reference to the secret containing credentials for registry authentication | -| `autoUpdateMiddleware` | boolean | No | Defines if the operator should rebuild when outdated middleware is detected. Defaults to global operator config | +| `autoUpdateMiddleware` | boolean | No | Defines if the operator should rebuild when outdated middleware is detected. When not specified, defaults to the operator-wide setting in the `func-operator-controller-config` ConfigMap (default: `true`). Function-level setting takes precedence over operator default | ### Function Status From c237f47dcc33d781872171f41083136167668f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Mon, 13 Apr 2026 20:44:22 +0200 Subject: [PATCH 03/10] Update controller to reconcile functions on controller-config configmap updates --- config/rbac/role.yaml | 8 ++ internal/controller/function_controller.go | 99 +++++++++++++++++----- 2 files changed, 85 insertions(+), 22 deletions(-) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 55cf981..d3f2546 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,14 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch - apiGroups: - "" resources: diff --git a/internal/controller/function_controller.go b/internal/controller/function_controller.go index 2df31a7..cb0a779 100644 --- a/internal/controller/function_controller.go +++ b/internal/controller/function_controller.go @@ -35,10 +35,13 @@ import ( "k8s.io/utils/ptr" funcfn "knative.dev/func/pkg/functions" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/functions-dev/func-operator/api/v1alpha1" rbacv1 "k8s.io/api/rbac/v1" @@ -47,7 +50,7 @@ import ( const ( deployFunctionRoleName = "func-operator-deploy-function" - controllerConfigName = "controller-config" + controllerConfigName = "func-operator-controller-config" ) // FunctionReconciler reconciles a Function object @@ -64,6 +67,7 @@ type FunctionReconciler struct { // +kubebuilder:rbac:groups=functions.dev,resources=functions/status,verbs=get;update;patch // +kubebuilder:rbac:groups=functions.dev,resources=functions/finalizers,verbs=update // +kubebuilder:rbac:groups="",resources=pods;pods/attach;secrets;services;persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch // +kubebuilder:rbac:groups="apps",resources=deployments;replicasets,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="serving.knative.dev",resources=services;routes,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="eventing.knative.dev",resources=triggers,verbs=get;list;watch;create;update;patch;delete @@ -226,39 +230,43 @@ func (r *FunctionReconciler) handleMiddlewareUpdate(ctx context.Context, functio if !isMiddlewareUpdateEnabled { logger.Info("Skipping middleware update, as middleware update is disabled") function.MarkMiddlewareNotUpToDateIntentionally("SkipMiddlewareUpdate", "Skipping middleware update as update is disabled (source: %s)", source) - return nil - } - - logger.Info("Function is not on latest middleware and middleware update is enabled. Will redeploy") - function.MarkMiddlewareNotUpToDate("MiddlewareOutdated", "Middleware is outdated, redeploying") + // Don't return - continue to update deployment status + } else { + logger.Info("Function is not on latest middleware and middleware update is enabled. Will redeploy") + function.MarkMiddlewareNotUpToDate("MiddlewareOutdated", "Middleware is outdated, redeploying") + + // update function image in status before long redeploy operation + functionDescribe, err := r.FuncCliManager.Describe(ctx, metadata.Name, function.Namespace) + if err != nil { + return fmt.Errorf("failed to describe function to get image details: %w", err) + } + function.Status.Deployment.Image = functionDescribe.Image - // update function image in status before long redeploy operation - functionDescribe, err := r.FuncCliManager.Describe(ctx, metadata.Name, function.Namespace) - if err != nil { - return fmt.Errorf("failed to describe function to get image details: %w", err) - } - function.Status.Deployment.Image = functionDescribe.Image + // Flush status before long deploy operation + if err := FlushStatus(ctx, function); err != nil { + logger.Error(err, "Failed to update status before redeployment") + } - // Flush status before long deploy operation - if err := FlushStatus(ctx, function); err != nil { - logger.Error(err, "Failed to update status before redeployment") - } + if err := r.deploy(ctx, function, repo); err != nil { + function.MarkDeployNotReady("DeployFailed", "Redeployment failed: %s", err.Error()) + return fmt.Errorf("failed to redeploy function: %w", err) + } - if err := r.deploy(ctx, function, repo); err != nil { - function.MarkDeployNotReady("DeployFailed", "Redeployment failed: %s", err.Error()) - return fmt.Errorf("failed to redeploy function: %w", err) + // After successful deployment, middleware is now up-to-date + function.MarkMiddlewareUpToDate() } } else { logger.Info("Function is deployed with latest middleware. No need to redeploy") + function.MarkMiddlewareUpToDate() } + // Update deployment status functionDescribe, err := r.FuncCliManager.Describe(ctx, metadata.Name, function.Namespace) if err != nil { return fmt.Errorf("failed to describe function to get image details: %w", err) } function.Status.Deployment.Image = functionDescribe.Image - function.MarkMiddlewareUpToDate() function.MarkDeployReady() return nil } @@ -484,8 +492,18 @@ func (r *FunctionReconciler) isDeployed(ctx context.Context, name, namespace str // SetupWithManager sets up the controller with the Manager. func (r *FunctionReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.Function{}). - WithEventFilter(predicate.GenerationChangedPredicate{}). // only reconcile when the spec changed (e.g. not on status updates) + // Only reconcile Functions when their spec changes (not on status updates). + // This predicate is applied to For() instead of WithEventFilter() to ensure + // it doesn't filter out ConfigMap-triggered reconciliations. + For(&v1alpha1.Function{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Watches( + &v1.ConfigMap{}, + handler.EnqueueRequestsFromMapFunc(r.findFunctionsForConfigMap), + builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool { + // Only watch the controller-config ConfigMap in the operator namespace + return obj.GetName() == controllerConfigName && obj.GetNamespace() == r.OperatorNamespace + })), + ). Named("function"). WithOptions(controller.Options{ MaxConcurrentReconciles: 100, // TODO: find a good value @@ -493,6 +511,43 @@ func (r *FunctionReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } +// findFunctionsForConfigMap returns reconcile requests for all Functions that should be +// reconciled when the controller-config ConfigMap changes. This triggers reconciliation +// for Functions that rely on the operator-wide default (i.e., those without an explicit +// autoUpdateMiddleware setting). +// +// Note: This function is safe for multi-controller setups. The List() call uses the manager's +// cached client, which is already scoped to the namespaces this controller is watching +// (via WATCH_NAMESPACE env var). Each controller instance only reconciles Functions in its +// own watched namespaces. +func (r *FunctionReconciler) findFunctionsForConfigMap(ctx context.Context, _ client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + // List all Functions in the watched namespaces (scoped by the manager's cache) + functionList := &v1alpha1.FunctionList{} + if err := r.List(ctx, functionList); err != nil { + logger.Error(err, "Failed to list Functions for ConfigMap watch") + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, 0, len(functionList.Items)) + for _, function := range functionList.Items { + // Only enqueue Functions that rely on the operator default + // (i.e., those without an explicit autoUpdateMiddleware setting) + if function.Spec.AutoUpdateMiddleware == nil { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: function.Name, + Namespace: function.Namespace, + }, + }) + } + } + + logger.Info("Enqueueing Functions for reconciliation due to ConfigMap change", "count", len(requests)) + return requests +} + func (r *FunctionReconciler) isMiddlewareLatest(ctx context.Context, metadata *funcfn.Function, namespace string) (bool, error) { latestMiddleware, err := r.FuncCliManager.GetLatestMiddlewareVersion(ctx, metadata.Runtime, metadata.Invoke) if err != nil { From 708b6a5ab77e6af285066786e7151a34c5c120e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Mon, 13 Apr 2026 20:44:31 +0200 Subject: [PATCH 04/10] Add e2e test --- test/e2e/func_middleware_update_test.go | 327 ++++++++++++++++++++++++ 1 file changed, 327 insertions(+) diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index 6e2d68f..d2899b2 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -30,7 +30,10 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gopkg.in/yaml.v3" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" funcfn "knative.dev/func/pkg/functions" ) @@ -241,4 +244,328 @@ var _ = Describe("Middleware Update", func() { Eventually(functionMiddlewareUpToDate(functionName, functionNamespace), 2*time.Minute).Should(Succeed()) }) }) + + Context("when ConfigMap autoUpdateMiddleware setting changes", func() { + const ( + operatorNamespace = "func-operator-system" + controllerConfigName = "func-operator-controller-config" + ) + + var repoURL string + var repoDir string + var functionName, functionNamespace string + var originalConfigMapData map[string]string + + BeforeEach(func() { + Skip("Skip for now, as the old used CLI for this test (1.20.1), does not have " + + "https://github.com/knative/func/pull/3490 yet") + + var err error + + // Save original ConfigMap data to restore later + cm := &v1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: controllerConfigName, + Namespace: operatorNamespace, + }, cm) + Expect(err).NotTo(HaveOccurred()) + originalConfigMapData = make(map[string]string) + for k, v := range cm.Data { + originalConfigMapData[k] = v + } + + // Restore original ConfigMap data on cleanup + DeferCleanup(func() { + By("Restoring original ConfigMap data") + cm := &v1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: controllerConfigName, + Namespace: operatorNamespace, + }, cm) + Expect(err).NotTo(HaveOccurred()) + + cm.Data = originalConfigMapData + err = k8sClient.Update(ctx, cm) + Expect(err).NotTo(HaveOccurred()) + }) + + // 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) + + functionNamespace, err = utils.GetTestNamespace() + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(cleanupNamespaces, functionNamespace) + + // Initialize repository with function code using OLD func CLI version + // to ensure middleware will be outdated + oldFuncVersion := "v1.20.1" + repoDir, err = utils.InitializeRepoWithFunction( + repoURL, + username, + password, + "go", + utils.WithCliVersion(oldFuncVersion)) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(os.RemoveAll, repoDir) + + // Deploy function using the same OLD func CLI version + out, err := utils.RunFuncDeploy(repoDir, + utils.WithNamespace(functionNamespace), + utils.WithDeployCliVersion(oldFuncVersion)) + 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() { + logFailedTestDetails(functionName, functionNamespace) + + // 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 reconcile functions without explicit autoUpdateMiddleware when ConfigMap changes", func() { + // Set ConfigMap to disable middleware updates + By("Disabling middleware updates in ConfigMap") + cm := &v1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: controllerConfigName, + Namespace: operatorNamespace, + }, cm) + Expect(err).NotTo(HaveOccurred()) + + cm.Data["autoUpdateMiddleware"] = "false" + err = k8sClient.Update(ctx, cm) + Expect(err).NotTo(HaveOccurred()) + + // Wait for the ConfigMap update to propagate + time.Sleep(2 * time.Second) + + // Create a Function resource WITHOUT autoUpdateMiddleware setting + // (so it will use the operator default) + fn := &functionsdevv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "my-function-", + Namespace: functionNamespace, + }, + Spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: repoURL, + }, + // NOTE: autoUpdateMiddleware is intentionally not set, + // so it will use the operator default (currently "false") + }, + } + + err = k8sClient.Create(ctx, fn) + Expect(err).NotTo(HaveOccurred()) + functionName = fn.Name + + By("Waiting for Function to become ready with middleware updates disabled") + Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed()) + + // Verify the MiddlewareUpToDate condition reflects that updates are disabled + By("Verifying middleware update is skipped due to ConfigMap setting") + Eventually(func(g Gomega) { + fn := &functionsdevv1alpha1.Function{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: functionName, + Namespace: functionNamespace, + }, fn) + g.Expect(err).NotTo(HaveOccurred()) + + // Find the MiddlewareUpToDate condition + for _, cond := range fn.Status.Conditions { + if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate { + // When middleware updates are disabled, the condition should be True + // with reason "SkipMiddlewareUpdate" + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal("SkipMiddlewareUpdate")) + g.Expect(cond.Message).To(ContainSubstring("operator")) + return + } + } + g.Expect(false).To(BeTrue(), "MiddlewareUpToDate condition not found") + }, 1*time.Minute).Should(Succeed()) + + // Get the initial image to compare later + fnBefore := &functionsdevv1alpha1.Function{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: functionName, + Namespace: functionNamespace, + }, fnBefore) + Expect(err).NotTo(HaveOccurred()) + imageBefore := fnBefore.Status.Deployment.Image + + // Now enable middleware updates in the ConfigMap + By("Enabling middleware updates in ConfigMap") + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: controllerConfigName, + Namespace: operatorNamespace, + }, cm) + Expect(err).NotTo(HaveOccurred()) + + cm.Data["autoUpdateMiddleware"] = "true" + err = k8sClient.Update(ctx, cm) + Expect(err).NotTo(HaveOccurred()) + + // The Function should be automatically reconciled and middleware should be updated + By("Waiting for Function to be reconciled and middleware updated after ConfigMap change") + Eventually(func(g Gomega) { + fn := &functionsdevv1alpha1.Function{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: functionName, + Namespace: functionNamespace, + }, fn) + g.Expect(err).NotTo(HaveOccurred()) + + // The image should have changed (middleware was updated) + g.Expect(fn.Status.Deployment.Image).NotTo(BeEmpty()) + g.Expect(fn.Status.Deployment.Image).NotTo(Equal(imageBefore), + "Image should have changed after middleware update") + + // The MiddlewareUpToDate condition should now indicate middleware is up to date + for _, cond := range fn.Status.Conditions { + if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate { + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal("MiddlewareUpToDate")) + return + } + } + g.Expect(false).To(BeTrue(), "MiddlewareUpToDate condition not found") + }, 5*time.Minute).Should(Succeed()) + + Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed()) + }) + + It("should not reconcile functions with explicit autoUpdateMiddleware when ConfigMap changes", func() { + // Set ConfigMap to enable middleware updates + By("Setting ConfigMap to enable middleware updates") + cm := &v1.ConfigMap{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: controllerConfigName, + Namespace: operatorNamespace, + }, cm) + Expect(err).NotTo(HaveOccurred()) + + cm.Data["autoUpdateMiddleware"] = "true" + err = k8sClient.Update(ctx, cm) + Expect(err).NotTo(HaveOccurred()) + + time.Sleep(2 * time.Second) + + // Create a Function resource WITH explicit autoUpdateMiddleware=false + // This should NOT be affected by ConfigMap changes + fn := &functionsdevv1alpha1.Function{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "my-function-explicit-", + Namespace: functionNamespace, + }, + Spec: functionsdevv1alpha1.FunctionSpec{ + Repository: functionsdevv1alpha1.FunctionSpecRepository{ + URL: repoURL, + }, + // Explicitly set autoUpdateMiddleware + AutoUpdateMiddleware: ptr.To(false), + }, + } + + err = k8sClient.Create(ctx, fn) + Expect(err).NotTo(HaveOccurred()) + functionName = fn.Name + + By("Waiting for Function to become ready") + Eventually(functionBecomesReady(functionName, functionNamespace)).Should(Succeed()) + + // Verify the MiddlewareUpToDate condition shows updates are skipped + By("Verifying middleware update is skipped due to function setting") + Eventually(func(g Gomega) { + fn := &functionsdevv1alpha1.Function{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: functionName, + Namespace: functionNamespace, + }, fn) + g.Expect(err).NotTo(HaveOccurred()) + + for _, cond := range fn.Status.Conditions { + if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate { + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal("SkipMiddlewareUpdate")) + // Message should indicate the source is the function spec + g.Expect(cond.Message).To(ContainSubstring("function")) + return + } + } + g.Expect(false).To(BeTrue(), "MiddlewareUpToDate condition not found") + }, 1*time.Minute).Should(Succeed()) + + // Get the current image + fnBefore := &functionsdevv1alpha1.Function{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: functionName, + Namespace: functionNamespace, + }, fnBefore) + Expect(err).NotTo(HaveOccurred()) + imageBefore := fnBefore.Status.Deployment.Image + + // Update ConfigMap to disable middleware updates + // (opposite of the function's explicit setting, but should have no effect) + By("Changing ConfigMap to disable middleware updates") + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: controllerConfigName, + Namespace: operatorNamespace, + }, cm) + Expect(err).NotTo(HaveOccurred()) + + cm.Data["autoUpdateMiddleware"] = "false" + err = k8sClient.Update(ctx, cm) + Expect(err).NotTo(HaveOccurred()) + + // Wait a bit to ensure controller has time to process ConfigMap change + time.Sleep(10 * time.Second) + + // The Function should NOT be reconciled and image should not change + By("Verifying Function was not reconciled after ConfigMap change") + Consistently(func(g Gomega) { + fn := &functionsdevv1alpha1.Function{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: functionName, + Namespace: functionNamespace, + }, fn) + g.Expect(err).NotTo(HaveOccurred()) + + // Image should not change + g.Expect(fn.Status.Deployment.Image).To(Equal(imageBefore)) + + // Condition should still show updates are skipped with "function" source + for _, cond := range fn.Status.Conditions { + if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate { + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal("SkipMiddlewareUpdate")) + g.Expect(cond.Message).To(ContainSubstring("function")) + return + } + } + g.Expect(false).To(BeTrue(), "MiddlewareUpToDate condition not found") + }, 30*time.Second, 5*time.Second).Should(Succeed()) + }) + }) }) From c41d6a4fd2975a3b68365be0dbe0d653a3e05874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Tue, 14 Apr 2026 08:15:35 +0200 Subject: [PATCH 05/10] Fix e2e operator log collection --- test/e2e/bundle_test.go | 2 +- test/e2e/e2e_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/bundle_test.go b/test/e2e/bundle_test.go index bc74396..651a262 100644 --- a/test/e2e/bundle_test.go +++ b/test/e2e/bundle_test.go @@ -66,7 +66,7 @@ var _ = Describe("Bundle", Label("bundle"), Ordered, func() { "kubectl", "logs", "-l", "control-plane=controller-manager", "--namespace", testNs.Name, - "-t", "-1") + "--tail", "20") controllerLogs, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 45d7d27..94b2358 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -236,7 +236,7 @@ func logFailedTestDetails(functionName, functionNamespace string) { } By("Fetching controller manager pod logs") - cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace, "-t", "20") + cmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace, "--tail", "20") controllerLogs, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) From 264679cf61be730b64efac989bf60d4edf09693d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Tue, 14 Apr 2026 08:16:32 +0200 Subject: [PATCH 06/10] Fix controller to be able to watch controller-config in its OWN namespace --- cmd/main.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index f1aac53..08b118e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,9 +28,11 @@ import ( "github.com/functions-dev/func-operator/internal/git" "github.com/functions-dev/func-operator/internal/monitoring" "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + v1 "k8s.io/api/core/v1" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/apimachinery/pkg/runtime" @@ -196,6 +198,12 @@ func main() { }) } + operatorNamespace := os.Getenv("SYSTEM_NAMESPACE") + if operatorNamespace == "" { + setupLog.Info("Operator namespace not set, defaulting to func-operator-system") + operatorNamespace = "func-operator-system" + } + watchNamespaces := getWatchNamespaces() var cacheOpts cache.Options if len(watchNamespaces) > 0 { @@ -210,6 +218,16 @@ func main() { setupLog.Info("Operator watching all namespaces") } + // Always watch ConfigMaps in the operator's namespace so it can access the controller-config ConfigMap, + // without affecting which namespaces Functions are watched in + cacheOpts.ByObject = map[client.Object]cache.ByObject{ + &v1.ConfigMap{}: { + Namespaces: map[string]cache.Config{ + operatorNamespace: {}, + }, + }, + } + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, @@ -251,12 +269,6 @@ func main() { } setupLog.Info("Func CLI is ready") - operatorNamespace := os.Getenv("SYSTEM_NAMESPACE") - if operatorNamespace == "" { - setupLog.Info("Operator namespace not set, defaulting to func-operator-system") - operatorNamespace = "func-operator-system" - } - if err := (&controller.FunctionReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), From 2edbabe10473749579746eccaaf71722a3462bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Tue, 14 Apr 2026 12:13:07 +0200 Subject: [PATCH 07/10] Enable middleware e2e tests Func 1.20.2 was released (containing https://github.com/knative/func/commit/d4f2a4060e765181878b58b85f050541053d7158 with the registry-insecure fix) --- test/e2e/func_middleware_update_test.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index d2899b2..80a6e72 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -49,8 +49,6 @@ var _ = Describe("Middleware Update", func() { var functionName, functionNamespace string BeforeEach(func() { - Skip("Skip for now, as the old used CLI for this test (1.20.1), does not have " + - "https://github.com/knative/func/pull/3490 yet") var err error // Create repository provider resources with automatic cleanup @@ -67,8 +65,8 @@ var _ = Describe("Middleware Update", func() { DeferCleanup(cleanupNamespaces, functionNamespace) // 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" + // v1.20.2 has no middleware-version label and uses instance-compatible templates + oldFuncVersion := "v1.20.2" repoDir, err = utils.InitializeRepoWithFunction( repoURL, username, @@ -128,7 +126,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 v1.20.1) + // Get initial image digest from func describe (deployed with v1.20.2) out, err := utils.RunFunc("describe", deployedFunctionName, "-n", functionNamespace, "-o", "yaml") Expect(err).NotTo(HaveOccurred()) @@ -138,9 +136,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.1): %s\n", initialImage) + _, _ = fmt.Fprintf(GinkgoWriter, "Initial image (deployed with v1.20.2): %s\n", initialImage) - // Verify initial image has no middleware-version label (v1.20.1 doesn't set it) + // Verify initial image has no middleware-version label (v1.20.2 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, "@") { @@ -169,7 +167,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.1)\n", + _, _ = fmt.Fprintf(GinkgoWriter, "Initial middleware-version label: '%s' (expected empty for v1.20.2)\n", initialMiddlewareVersion) // Create a Function resource @@ -257,9 +255,6 @@ var _ = Describe("Middleware Update", func() { var originalConfigMapData map[string]string BeforeEach(func() { - Skip("Skip for now, as the old used CLI for this test (1.20.1), does not have " + - "https://github.com/knative/func/pull/3490 yet") - var err error // Save original ConfigMap data to restore later @@ -304,7 +299,7 @@ var _ = Describe("Middleware Update", func() { // Initialize repository with function code using OLD func CLI version // to ensure middleware will be outdated - oldFuncVersion := "v1.20.1" + oldFuncVersion := "v1.20.2" repoDir, err = utils.InitializeRepoWithFunction( repoURL, username, From 3fae43ece8444d0242895d8b4578fd413542508a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Tue, 14 Apr 2026 13:32:16 +0200 Subject: [PATCH 08/10] Skip middleware tests for keda&raw deployer The old func CLI in the tests (1.20.2) does not support the Keda/raw deployers. But the tests are OK to run with the knative deployer --- test/e2e/func_middleware_update_test.go | 10 ++++++++++ test/utils/func.go | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index 80a6e72..0716a82 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -49,6 +49,11 @@ var _ = Describe("Middleware Update", func() { var functionName, functionNamespace string BeforeEach(func() { + if os.Getenv("DEFAULT_DEPLOYER") == "keda" || os.Getenv("DEFAULT_DEPLOYER") == "raw" { + Skip("Skipping middleware test for Keda & raw deployer, " + + "as those are not supported on used CLI version (1.20.x) of this tests") + } + var err error // Create repository provider resources with automatic cleanup @@ -255,6 +260,11 @@ var _ = Describe("Middleware Update", func() { var originalConfigMapData map[string]string BeforeEach(func() { + if os.Getenv("DEFAULT_DEPLOYER") == "keda" || os.Getenv("DEFAULT_DEPLOYER") == "raw" { + Skip("Skipping middleware test for Keda & raw deployer, " + + "as those are not supported on used CLI version (1.20.x) of this tests") + } + var err error // Save original ConfigMap data to restore later diff --git a/test/utils/func.go b/test/utils/func.go index f720fdf..d315848 100644 --- a/test/utils/func.go +++ b/test/utils/func.go @@ -74,7 +74,7 @@ func RunFuncDeploy(functionDir string, optFns ...FuncDeployOption) (string, erro args = append(args, "--builder", opts.Builder) } - if opts.Deployer != "" { + if opts.Deployer != "" && opts.Deployer != "knative" { args = append(args, "--deployer", opts.Deployer) } From d533a05d536722ccb3f6eb91b50dd4cf5ab9dd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Tue, 14 Apr 2026 15:05:26 +0200 Subject: [PATCH 09/10] Fix e2e test --- test/e2e/func_middleware_update_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index 0716a82..ce89e9d 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -451,7 +451,7 @@ var _ = Describe("Middleware Update", func() { for _, cond := range fn.Status.Conditions { if cond.Type == functionsdevv1alpha1.TypeMiddlewareUpToDate { g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - g.Expect(cond.Reason).To(Equal("MiddlewareUpToDate")) + g.Expect(cond.Reason).To(Equal("UpToDate")) return } } From 8271794afbc523b34167df4b327ee90ecf8ef188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Tue, 14 Apr 2026 22:39:55 +0200 Subject: [PATCH 10/10] Skip middleware tests for unsupported builders --- test/e2e/func_middleware_update_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/e2e/func_middleware_update_test.go b/test/e2e/func_middleware_update_test.go index ce89e9d..101e034 100644 --- a/test/e2e/func_middleware_update_test.go +++ b/test/e2e/func_middleware_update_test.go @@ -54,6 +54,11 @@ var _ = Describe("Middleware Update", func() { "as those are not supported on used CLI version (1.20.x) of this tests") } + if os.Getenv("DEFAULT_BUILDER") == "pack" { + Skip("Skipping middleware test for pack builder, " + + "as it does not add the middleware version label on remote builds yet") + } + var err error // Create repository provider resources with automatic cleanup @@ -265,6 +270,11 @@ var _ = Describe("Middleware Update", func() { "as those are not supported on used CLI version (1.20.x) of this tests") } + if os.Getenv("DEFAULT_BUILDER") == "pack" { + Skip("Skipping middleware test for pack builder, " + + "as it does not add the middleware version label on remote builds yet") + } + var err error // Save original ConfigMap data to restore later