diff --git a/cmd/func-operator/main.go b/cmd/func-operator/main.go index 9da2f5d..f323891 100644 --- a/cmd/func-operator/main.go +++ b/cmd/func-operator/main.go @@ -81,6 +81,7 @@ type cliFlags struct { funcCLIPath string funcCLICheckInterval time.Duration disableFuncCLIUpdate bool + createConfig bool } func parseFlags() cliFlags { @@ -108,6 +109,8 @@ func parseFlags() cliFlags { flag.DurationVar(&flags.funcCLICheckInterval, "func-cli-check-interval", 5*time.Minute, "How often to check for new func CLI versions") flag.BoolVar(&flags.disableFuncCLIUpdate, "disable-func-cli-update", false, "Disable the function-cli update") + flag.BoolVar(&flags.createConfig, "create-config", false, + "If set, create the default controller-config ConfigMap at startup if it does not already exist.") opts := zap.Options{ Development: true, } @@ -304,6 +307,18 @@ func main() { metricsServerOptions := setupMetricsServerOptions(flags.metricsAddr, flags.secureMetrics, metricsCertWatcher, tlsOpts) operatorNamespace := getOperatorNamespace() + + // Optionally create the default controller-config ConfigMap before the manager + // starts. This is used when deploying via OLM, where the default ConfigMap is + // not part of the bundle. When deploying via the config/ manifests, the + // ConfigMap is managed there and this flag is left unset. + if flags.createConfig { + if err := controller.EnsureDefaultConfigMap(context.Background(), operatorNamespace); err != nil { + setupLog.Error(err, "failed to ensure default controller-config ConfigMap") + os.Exit(1) + } + } + cacheOpts := setupCacheOptions(operatorNamespace) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ diff --git a/internal/controller/controller_config.go b/internal/controller/controller_config.go new file mode 100644 index 0000000..b7f9d94 --- /dev/null +++ b/internal/controller/controller_config.go @@ -0,0 +1,80 @@ +/* +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 controller + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// defaultAutoUpdateMiddleware mirrors the value shipped in +// config/manager/manager.yaml. It is used when creating the controller-config +// ConfigMap for deployments that do not provide one (e.g. OLM bundles that omit +// the default ConfigMap). +const defaultAutoUpdateMiddleware = "true" + +// EnsureDefaultConfigMap creates the controller-config ConfigMap with default +// values if it does not already exist. It is a no-op when the ConfigMap is +// already present. This allows the operator to be deployed via OLM, where the +// default ConfigMap is not part of the bundle. +func EnsureDefaultConfigMap(ctx context.Context, namespace string) error { + clientset, err := kubernetes.NewForConfig(ctrl.GetConfigOrDie()) + if err != nil { + return fmt.Errorf("creating kubernetes clientset: %w", err) + } + + _, err = clientset.CoreV1().ConfigMaps(namespace).Get(ctx, controllerConfigName, metav1.GetOptions{}) + if err == nil { + log.Log.Info("controller-config ConfigMap already exists, not creating", + "name", controllerConfigName, "namespace", namespace) + return nil + } + if !apierrors.IsNotFound(err) { + return fmt.Errorf("checking for ConfigMap %s/%s: %w", namespace, controllerConfigName, err) + } + + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerConfigName, + Namespace: namespace, + }, + Data: map[string]string{ + "autoUpdateMiddleware": defaultAutoUpdateMiddleware, + }, + } + + if _, err := clientset.CoreV1().ConfigMaps(namespace).Create(ctx, cm, metav1.CreateOptions{}); err != nil { + if apierrors.IsAlreadyExists(err) { + // Another replica created it concurrently; treat as success. + log.Log.Info("controller-config ConfigMap already created concurrently", + "name", controllerConfigName, "namespace", namespace) + return nil + } + return fmt.Errorf("creating default ConfigMap %s/%s: %w", namespace, controllerConfigName, err) + } + + log.Log.Info("created default controller-config ConfigMap", + "name", controllerConfigName, "namespace", namespace) + return nil +} diff --git a/internal/controller/function_controller.go b/internal/controller/function_controller.go index f413de6..dbc549b 100644 --- a/internal/controller/function_controller.go +++ b/internal/controller/function_controller.go @@ -70,7 +70,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="",resources=configmaps,verbs=get;list;watch;create // +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