Skip to content

Commit e221667

Browse files
authored
Add possibility to specify operator default for middleware update enablement (#45)
1 parent 922c551 commit e221667

12 files changed

Lines changed: 661 additions & 45 deletions

File tree

‎README.md‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,42 @@ spec:
158158

159159
The operator will clone the repository and use the specified path as the function root directory.
160160

161+
### Configuring Automatic Middleware Updates
162+
163+
The operators main responsibility it to rebuild functions when outdated middleware is detected. Anyhow this behavior can be enabled/disabled at two levels:
164+
165+
#### Operator-Level Default
166+
167+
Configure the operator-wide default by editing the `func-operator-controller-config` ConfigMap in the operators namespace (`func-operator-system` by default):
168+
169+
```yaml
170+
apiVersion: v1
171+
kind: ConfigMap
172+
metadata:
173+
name: func-operator-controller-config
174+
namespace: func-operator-system
175+
data:
176+
autoUpdateMiddleware: "true" # or "false" to disable by default
177+
```
178+
179+
#### Per-Function Override
180+
181+
Individual functions can override the operator default using the `autoUpdateMiddleware` field:
182+
183+
```yaml
184+
apiVersion: functions.dev/v1alpha1
185+
kind: Function
186+
metadata:
187+
name: my-function
188+
namespace: default
189+
spec:
190+
repository:
191+
url: https://github.com/your-org/your-function.git
192+
autoUpdateMiddleware: false # Disable middleware updates for this function
193+
```
194+
195+
**Precedence:** Function-level settings always take priority over the operator default.
196+
161197
## Development
162198

163199
### Local Development Cluster
@@ -243,7 +279,7 @@ make lint
243279
| `repository.path` | string | No | Path to the function inside the repository. Defaults to "." |
244280
| `repository.authSecretRef` | object | No | Reference to the auth secret for private repository authentication |
245281
| `registry.authSecretRef` | object | No | Reference to the secret containing credentials for registry authentication |
246-
| `autoUpdateMiddleware` | boolean | No | Defines if the operator should rebuild when outdated middleware is detected. Defaults to global operator config |
282+
| `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 |
247283

248284
### Function Status
249285

‎api/v1alpha1/function_lifecycle.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,13 @@ func (f *Function) MarkMiddlewareNotUpToDate(reason, messageFormat string, messa
184184
ObservedGeneration: f.Generation,
185185
})
186186
}
187+
188+
func (f *Function) MarkMiddlewareNotUpToDateIntentionally(reason, messageFormat string, messageA ...interface{}) bool {
189+
return meta.SetStatusCondition(&f.Status.Conditions, metav1.Condition{
190+
Type: TypeMiddlewareUpToDate,
191+
Status: metav1.ConditionTrue,
192+
Reason: reason,
193+
Message: fmt.Sprintf(messageFormat, messageA...),
194+
ObservedGeneration: f.Generation,
195+
})
196+
}

‎api/v1alpha1/function_types.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ type FunctionSpec struct {
4343

4444
// AutoUpdateMiddleware defines if the operator should rebuild the function when an outdated middleware is detected.
4545
// Defaults to the global operator config.
46-
// TODO: implement logic
4746
AutoUpdateMiddleware *bool `json:"autoUpdateMiddleware,omitempty"`
4847
}
4948

‎cmd/main.go‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import (
2828
"github.com/functions-dev/func-operator/internal/git"
2929
"github.com/functions-dev/func-operator/internal/monitoring"
3030
"sigs.k8s.io/controller-runtime/pkg/cache"
31+
"sigs.k8s.io/controller-runtime/pkg/client"
3132

3233
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
3334
// to ensure that exec-entrypoint and run can make use of them.
35+
v1 "k8s.io/api/core/v1"
3436
_ "k8s.io/client-go/plugin/pkg/client/auth"
3537

3638
"k8s.io/apimachinery/pkg/runtime"
@@ -196,6 +198,12 @@ func main() {
196198
})
197199
}
198200

201+
operatorNamespace := os.Getenv("SYSTEM_NAMESPACE")
202+
if operatorNamespace == "" {
203+
setupLog.Info("Operator namespace not set, defaulting to func-operator-system")
204+
operatorNamespace = "func-operator-system"
205+
}
206+
199207
watchNamespaces := getWatchNamespaces()
200208
var cacheOpts cache.Options
201209
if len(watchNamespaces) > 0 {
@@ -210,6 +218,16 @@ func main() {
210218
setupLog.Info("Operator watching all namespaces")
211219
}
212220

221+
// Always watch ConfigMaps in the operator's namespace so it can access the controller-config ConfigMap,
222+
// without affecting which namespaces Functions are watched in
223+
cacheOpts.ByObject = map[client.Object]cache.ByObject{
224+
&v1.ConfigMap{}: {
225+
Namespaces: map[string]cache.Config{
226+
operatorNamespace: {},
227+
},
228+
},
229+
}
230+
213231
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
214232
Scheme: scheme,
215233
Metrics: metricsServerOptions,
@@ -252,11 +270,12 @@ func main() {
252270
setupLog.Info("Func CLI is ready")
253271

254272
if err := (&controller.FunctionReconciler{
255-
Client: mgr.GetClient(),
256-
Scheme: mgr.GetScheme(),
257-
Recorder: mgr.GetEventRecorder("functions-controller"),
258-
FuncCliManager: funcCLIManager,
259-
GitManager: git.NewManager(),
273+
Client: mgr.GetClient(),
274+
Scheme: mgr.GetScheme(),
275+
Recorder: mgr.GetEventRecorder("functions-controller"),
276+
FuncCliManager: funcCLIManager,
277+
GitManager: git.NewManager(),
278+
OperatorNamespace: operatorNamespace,
260279
}).SetupWithManager(mgr); err != nil {
261280
setupLog.Error(err, "unable to create controller", "controller", "Function")
262281
os.Exit(1)

‎config/manager/manager.yaml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ metadata:
77
app.kubernetes.io/managed-by: kustomize
88
name: system
99
---
10+
apiVersion: v1
11+
kind: ConfigMap
12+
metadata:
13+
name: controller-config
14+
namespace: system
15+
labels:
16+
control-plane: controller-manager
17+
app.kubernetes.io/name: func-operator
18+
app.kubernetes.io/managed-by: kustomize
19+
data:
20+
# default to enable middleware updates
21+
autoUpdateMiddleware: "true"
22+
---
1023
apiVersion: apps/v1
1124
kind: Deployment
1225
metadata:
@@ -70,6 +83,10 @@ spec:
7083
valueFrom:
7184
fieldRef:
7285
fieldPath: metadata.annotations['olm.targetNamespaces']
86+
- name: SYSTEM_NAMESPACE
87+
valueFrom:
88+
fieldRef:
89+
fieldPath: metadata.namespace
7390
image: controller:latest
7491
imagePullPolicy: Always
7592
name: manager

‎config/rbac/role.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ kind: ClusterRole
44
metadata:
55
name: manager-role
66
rules:
7+
- apiGroups:
8+
- ""
9+
resources:
10+
- configmaps
11+
verbs:
12+
- get
13+
- list
14+
- watch
715
- apiGroups:
816
- ""
917
resources:

‎internal/controller/function_controller.go‎

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"os"
23+
"strconv"
2324
"strings"
2425

2526
"github.com/functions-dev/func-operator/internal/funccli"
@@ -34,10 +35,13 @@ import (
3435
"k8s.io/utils/ptr"
3536
funcfn "knative.dev/func/pkg/functions"
3637
ctrl "sigs.k8s.io/controller-runtime"
38+
"sigs.k8s.io/controller-runtime/pkg/builder"
3739
"sigs.k8s.io/controller-runtime/pkg/client"
3840
"sigs.k8s.io/controller-runtime/pkg/controller"
41+
"sigs.k8s.io/controller-runtime/pkg/handler"
3942
"sigs.k8s.io/controller-runtime/pkg/log"
4043
"sigs.k8s.io/controller-runtime/pkg/predicate"
44+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4145

4246
"github.com/functions-dev/func-operator/api/v1alpha1"
4347
rbacv1 "k8s.io/api/rbac/v1"
@@ -46,21 +50,24 @@ import (
4650

4751
const (
4852
deployFunctionRoleName = "func-operator-deploy-function"
53+
controllerConfigName = "func-operator-controller-config"
4954
)
5055

5156
// FunctionReconciler reconciles a Function object
5257
type FunctionReconciler struct {
5358
client.Client
54-
Scheme *runtime.Scheme
55-
Recorder events.EventRecorder
56-
FuncCliManager funccli.Manager
57-
GitManager git.Manager
59+
Scheme *runtime.Scheme
60+
Recorder events.EventRecorder
61+
FuncCliManager funccli.Manager
62+
GitManager git.Manager
63+
OperatorNamespace string
5864
}
5965

6066
// +kubebuilder:rbac:groups=functions.dev,resources=functions,verbs=get;list;watch;create;update;patch;delete
6167
// +kubebuilder:rbac:groups=functions.dev,resources=functions/status,verbs=get;update;patch
6268
// +kubebuilder:rbac:groups=functions.dev,resources=functions/finalizers,verbs=update
6369
// +kubebuilder:rbac:groups="",resources=pods;pods/attach;secrets;services;persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
70+
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
6471
// +kubebuilder:rbac:groups="apps",resources=deployments;replicasets,verbs=get;list;watch;create;update;patch;delete
6572
// +kubebuilder:rbac:groups="serving.knative.dev",resources=services;routes,verbs=get;list;watch;create;update;patch;delete
6673
// +kubebuilder:rbac:groups="eventing.knative.dev",resources=triggers,verbs=get;list;watch;create;update;patch;delete
@@ -214,36 +221,52 @@ func (r *FunctionReconciler) handleMiddlewareUpdate(ctx context.Context, functio
214221
}
215222

216223
if !isOnLatestMiddleware {
217-
logger.Info("Function is not on latest middleware. Will redeploy")
218-
function.MarkMiddlewareNotUpToDate("MiddlewareOutdated", "Middleware is outdated, redeploying")
219-
220-
// update function image in status before long redeploy operation
221-
functionDescribe, err := r.FuncCliManager.Describe(ctx, metadata.Name, function.Namespace)
224+
isMiddlewareUpdateEnabled, source, err := r.isMiddlewareUpdateEnabled(ctx, function)
222225
if err != nil {
223-
return fmt.Errorf("failed to describe function to get image details: %w", err)
226+
function.MarkMiddlewareNotUpToDate("MiddlewareCheckFailed", "Failed to check if middleware should be updated: %s", err)
227+
return fmt.Errorf("failed to check if middleware should be updated: %w", err)
224228
}
225-
function.Status.Deployment.Image = functionDescribe.Image
226229

227-
// Flush status before long deploy operation
228-
if err := FlushStatus(ctx, function); err != nil {
229-
logger.Error(err, "Failed to update status before redeployment")
230-
}
230+
if !isMiddlewareUpdateEnabled {
231+
logger.Info("Skipping middleware update, as middleware update is disabled")
232+
function.MarkMiddlewareNotUpToDateIntentionally("SkipMiddlewareUpdate", "Skipping middleware update as update is disabled (source: %s)", source)
233+
// Don't return - continue to update deployment status
234+
} else {
235+
logger.Info("Function is not on latest middleware and middleware update is enabled. Will redeploy")
236+
function.MarkMiddlewareNotUpToDate("MiddlewareOutdated", "Middleware is outdated, redeploying")
237+
238+
// update function image in status before long redeploy operation
239+
functionDescribe, err := r.FuncCliManager.Describe(ctx, metadata.Name, function.Namespace)
240+
if err != nil {
241+
return fmt.Errorf("failed to describe function to get image details: %w", err)
242+
}
243+
function.Status.Deployment.Image = functionDescribe.Image
244+
245+
// Flush status before long deploy operation
246+
if err := FlushStatus(ctx, function); err != nil {
247+
logger.Error(err, "Failed to update status before redeployment")
248+
}
249+
250+
if err := r.deploy(ctx, function, repo); err != nil {
251+
function.MarkDeployNotReady("DeployFailed", "Redeployment failed: %s", err.Error())
252+
return fmt.Errorf("failed to redeploy function: %w", err)
253+
}
231254

232-
if err := r.deploy(ctx, function, repo); err != nil {
233-
function.MarkDeployNotReady("DeployFailed", "Redeployment failed: %s", err.Error())
234-
return fmt.Errorf("failed to redeploy function: %w", err)
255+
// After successful deployment, middleware is now up-to-date
256+
function.MarkMiddlewareUpToDate()
235257
}
236258
} else {
237259
logger.Info("Function is deployed with latest middleware. No need to redeploy")
260+
function.MarkMiddlewareUpToDate()
238261
}
239262

263+
// Update deployment status
240264
functionDescribe, err := r.FuncCliManager.Describe(ctx, metadata.Name, function.Namespace)
241265
if err != nil {
242266
return fmt.Errorf("failed to describe function to get image details: %w", err)
243267
}
244268
function.Status.Deployment.Image = functionDescribe.Image
245269

246-
function.MarkMiddlewareUpToDate()
247270
function.MarkDeployReady()
248271
return nil
249272
}
@@ -469,15 +492,62 @@ func (r *FunctionReconciler) isDeployed(ctx context.Context, name, namespace str
469492
// SetupWithManager sets up the controller with the Manager.
470493
func (r *FunctionReconciler) SetupWithManager(mgr ctrl.Manager) error {
471494
return ctrl.NewControllerManagedBy(mgr).
472-
For(&v1alpha1.Function{}).
473-
WithEventFilter(predicate.GenerationChangedPredicate{}). // only reconcile when the spec changed (e.g. not on status updates)
495+
// Only reconcile Functions when their spec changes (not on status updates).
496+
// This predicate is applied to For() instead of WithEventFilter() to ensure
497+
// it doesn't filter out ConfigMap-triggered reconciliations.
498+
For(&v1alpha1.Function{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
499+
Watches(
500+
&v1.ConfigMap{},
501+
handler.EnqueueRequestsFromMapFunc(r.findFunctionsForConfigMap),
502+
builder.WithPredicates(predicate.NewPredicateFuncs(func(obj client.Object) bool {
503+
// Only watch the controller-config ConfigMap in the operator namespace
504+
return obj.GetName() == controllerConfigName && obj.GetNamespace() == r.OperatorNamespace
505+
})),
506+
).
474507
Named("function").
475508
WithOptions(controller.Options{
476509
MaxConcurrentReconciles: 100, // TODO: find a good value
477510
}).
478511
Complete(r)
479512
}
480513

514+
// findFunctionsForConfigMap returns reconcile requests for all Functions that should be
515+
// reconciled when the controller-config ConfigMap changes. This triggers reconciliation
516+
// for Functions that rely on the operator-wide default (i.e., those without an explicit
517+
// autoUpdateMiddleware setting).
518+
//
519+
// Note: This function is safe for multi-controller setups. The List() call uses the manager's
520+
// cached client, which is already scoped to the namespaces this controller is watching
521+
// (via WATCH_NAMESPACE env var). Each controller instance only reconciles Functions in its
522+
// own watched namespaces.
523+
func (r *FunctionReconciler) findFunctionsForConfigMap(ctx context.Context, _ client.Object) []reconcile.Request {
524+
logger := log.FromContext(ctx)
525+
526+
// List all Functions in the watched namespaces (scoped by the manager's cache)
527+
functionList := &v1alpha1.FunctionList{}
528+
if err := r.List(ctx, functionList); err != nil {
529+
logger.Error(err, "Failed to list Functions for ConfigMap watch")
530+
return []reconcile.Request{}
531+
}
532+
533+
requests := make([]reconcile.Request, 0, len(functionList.Items))
534+
for _, function := range functionList.Items {
535+
// Only enqueue Functions that rely on the operator default
536+
// (i.e., those without an explicit autoUpdateMiddleware setting)
537+
if function.Spec.AutoUpdateMiddleware == nil {
538+
requests = append(requests, reconcile.Request{
539+
NamespacedName: types.NamespacedName{
540+
Name: function.Name,
541+
Namespace: function.Namespace,
542+
},
543+
})
544+
}
545+
}
546+
547+
logger.Info("Enqueueing Functions for reconciliation due to ConfigMap change", "count", len(requests))
548+
return requests
549+
}
550+
481551
func (r *FunctionReconciler) isMiddlewareLatest(ctx context.Context, metadata *funcfn.Function, namespace string) (bool, error) {
482552
latestMiddleware, err := r.FuncCliManager.GetLatestMiddlewareVersion(ctx, metadata.Runtime, metadata.Invoke)
483553
if err != nil {
@@ -491,3 +561,35 @@ func (r *FunctionReconciler) isMiddlewareLatest(ctx context.Context, metadata *f
491561

492562
return latestMiddleware == functionMiddleware, nil
493563
}
564+
565+
// isMiddlewareUpdateEnabled returns if the middleware should be updated given by the functions spec or the operators
566+
// default.
567+
func (r *FunctionReconciler) isMiddlewareUpdateEnabled(ctx context.Context, function *v1alpha1.Function) (bool, string, error) {
568+
logger := log.FromContext(ctx)
569+
570+
// setting from function overrides operator default
571+
if function.Spec.AutoUpdateMiddleware != nil {
572+
return *function.Spec.AutoUpdateMiddleware, "function", nil
573+
}
574+
575+
// nothing defined in function spec --> check operator config
576+
cm := &v1.ConfigMap{}
577+
err := r.Get(ctx, types.NamespacedName{Namespace: r.OperatorNamespace, Name: controllerConfigName}, cm)
578+
if err != nil {
579+
return false, "", fmt.Errorf("failed to get operator config configmap: %w", err)
580+
}
581+
582+
val, ok := cm.Data["autoUpdateMiddleware"]
583+
if !ok {
584+
logger.Info("No autoUpdateMiddleware field in configmap found. Fallback to hardcoded autoUpdateMiddleware=true")
585+
// TODO: check if returning an error would be better here
586+
return true, "operator", nil
587+
}
588+
589+
boolVal, err := strconv.ParseBool(val)
590+
if err != nil {
591+
return false, "", fmt.Errorf("failed to parse autoUpdateMiddleware value from configmap: %w", err)
592+
}
593+
594+
return boolVal, "operator", nil
595+
}

0 commit comments

Comments
 (0)