Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/operator/conf/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Flag struct {
MetricsAddr string
ProbeAddr string
Namespace string
NamespaceSelector string
EnableLeaderElection bool
PrintVar bool
EnableWebhook bool
Expand All @@ -39,6 +40,7 @@ func ParseFlags() *Flag {
flag.StringVar(&f.MetricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&f.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.StringVar(&f.Namespace, "namespace", v12.NamespaceAll, "The namespace to watch for changes.")
flag.StringVar(&f.NamespaceSelector, "namespace-selector", "", "Only reconcile namespaces matching this label selector; cannot be combined with --namespace.")
flag.BoolVar(&f.EnableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
Expand Down
10 changes: 10 additions & 0 deletions cmd/operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/apache/doris-operator/cmd/operator/conf"
"github.com/apache/doris-operator/pkg/common/utils/certificate"
"github.com/apache/doris-operator/pkg/controller"
"github.com/apache/doris-operator/pkg/controller/namespacescope"
"github.com/apache/doris-operator/pkg/controller/unnamedwatches"
"io"
"k8s.io/apimachinery/pkg/util/wait"
Expand Down Expand Up @@ -117,6 +118,14 @@ func main() {
webhookServer := webhook.NewServer(webhook.Options{
Port: 9443,
})
if f.NamespaceSelector != "" && (f.Namespace != "" || envs.EnableWebhook) {
setupLog.Error(fmt.Errorf("namespace-selector requires global cache and ENABLE_WEBHOOK=false"), "invalid scope")
os.Exit(1)
}
if _, err := namespacescope.New(nil, f.NamespaceSelector); err != nil {
setupLog.Error(err, "invalid namespace-selector")
os.Exit(1)
}
defaultNamespaces := map[string]cache.Config{}
if f.Namespace != "" {
defaultNamespaces[f.Namespace] = cache.Config{}
Expand Down Expand Up @@ -157,6 +166,7 @@ func main() {
}

options := conf.NewControllerOptions(envs)
options.NamespaceScope, _ = namespacescope.New(mgr.GetAPIReader(), f.NamespaceSelector)
//initial all controllers
for _, c := range controller.Controllers {
c.Init(mgr, options)
Expand Down
45 changes: 45 additions & 0 deletions doc/namespace-selector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Sharing controllers across namespaces

A controller can reconcile a dynamically selected group of namespaces:

```yaml
# Controller Deployment
args:
- --leader-elect
- --namespace-selector=selectdb.com/operator-group=qa
env:
- name: ENABLE_WEBHOOK
value: "false"
```

Label business namespaces before creating Doris CRs:

```yaml
apiVersion: v1
kind: Namespace
metadata:
name: qa-run-123
labels:
selectdb.com/operator-group: qa
```

Run another group with a disjoint selector and a separate controller namespace.
Groups must not share the default leader-election Lease. An unfiltered global
controller would still overlap all groups and must not run alongside them.

`--namespace` and `--namespace-selector` are mutually exclusive. Selector mode
requires webhooks disabled and namespace get/list/watch permission, supplied by
the Helm Chart. The Chart exposes `dorisOperator.namespaceSelector` and disables
webhooks for this mode. Empty configuration preserves the existing global or
single-namespace behavior.

The controller reads namespace labels before each Doris reconciliation. Read
errors stop reconciliation; removing the group label stops future reconciliations.
Namespace label events enqueue existing CRs, so a newly labeled namespace is
adopted without restarting the controller. Label selectors are Kubernetes selectors;
the platform deploying multiple controllers is responsible for making them disjoint.
Do not transfer namespaces between groups while CRs/finalizers remain.

The global read cache remains available for explicit dependencies such as another
namespace's FDB connection ConfigMap. This filters reconciliation, not authorization;
use RBAC and separate clusters when read isolation or incompatible CRDs are required.
3 changes: 3 additions & 0 deletions helm-charts/doris-operator/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ metadata:
creationTimestamp: null
name: {{ template "kube-doris.name" . }}-operator
rules:
- apiGroups: [""]
resources: [namespaces]
verbs: [get, list, watch]
- apiGroups:
- apps
resources:
Expand Down
5 changes: 4 additions & 1 deletion helm-charts/doris-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ spec:
- /dorisoperator
args:
- --leader-elect
{{- if .Values.dorisOperator.namespaceSelector }}
- {{ printf "--namespace-selector=%s" .Values.dorisOperator.namespaceSelector | quote }}
{{- end }}
image: {{ .Values.dorisOperator.image.repository }}:{{ .Values.dorisOperator.image.tag }}
{{- if .Values.dorisOperator.image.imagePullPolicy }}
imagePullPolicy: {{ .Values.dorisOperator.image.imagePullPolicy }}
Expand All @@ -93,7 +96,7 @@ spec:
- "ALL"
env:
- name: ENABLE_WEBHOOK
value: "{{ template "webhook.enable" . }}"
value: {{ if .Values.dorisOperator.namespaceSelector }}"false"{{ else }}"{{ template "webhook.enable" . }}"{{ end }}
- name: START_DISAGGREGATED_OPERATOR
value: "true"
- name: OPERATOR_NAMESPACE
Expand Down
2 changes: 2 additions & 0 deletions helm-charts/doris-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@


dorisOperator:
# Dynamic namespace label selection. Requires a build supporting --namespace-selector.
namespaceSelector: ""
# the namespace of doris operator deployed, is not provided be set in values.yaml, please use the helm command specify, example "helm install -n doris"
image:
repository: apache/doris
Expand Down
15 changes: 12 additions & 3 deletions pkg/controller/disaggregated_cluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
dv1 "github.com/apache/doris-operator/api/disaggregated/v1"
"github.com/apache/doris-operator/pkg/common/utils/hash"
"github.com/apache/doris-operator/pkg/common/utils/runtimeconfig"
"github.com/apache/doris-operator/pkg/controller/namespacescope"
sc "github.com/apache/doris-operator/pkg/controller/sub_controller"
dcgs "github.com/apache/doris-operator/pkg/controller/sub_controller/disaggregated_cluster/computegroups"
dfe "github.com/apache/doris-operator/pkg/controller/sub_controller/disaggregated_cluster/disaggregated_fe"
Expand Down Expand Up @@ -58,6 +59,7 @@ var (
)

type DisaggregatedClusterReconciler struct {
NamespaceScope *namespacescope.Scope
client.Client
Recorder record.EventRecorder
Scheme *runtime.Scheme
Expand All @@ -75,9 +77,10 @@ func (dc *DisaggregatedClusterReconciler) Init(mgr ctrl.Manager, options *Option
scs[dccsc.GetControllerName()] = dccsc

if err := (&DisaggregatedClusterReconciler{
Client: mgr.GetClient(),
Recorder: mgr.GetEventRecorderFor(disaggregatedClusterController),
Scs: scs,
NamespaceScope: options.NamespaceScope,
Client: mgr.GetClient(),
Recorder: mgr.GetEventRecorderFor(disaggregatedClusterController),
Scs: scs,
}).SetupWithManager(mgr); err != nil {
klog.Error(err, "unable to create controller ", "disaggregatedClusterReconciler")
os.Exit(1)
Expand All @@ -95,6 +98,9 @@ func (dc *DisaggregatedClusterReconciler) SetupWithManager(mgr ctrl.Manager) err
builder := dc.resourceBuilder(ctrl.NewControllerManagedBy(mgr))
builder = dc.watchPodBuilder(builder)
builder = dc.watchFDBConfigMapBuilder(builder)
if dc.NamespaceScope.Enabled() {
builder = namespacescope.Watch(builder, mgr.GetClient(), func() client.ObjectList { return &dv1.DorisDisaggregatedClusterList{} })
}
return builder.Complete(dc)
}

Expand Down Expand Up @@ -215,6 +221,9 @@ func (dc *DisaggregatedClusterReconciler) resourceBuilder(builder *ctrl.Builder)
// 3. clear need delete resource.
// 4. display new status(eorganize status, update cr or status)
func (dc *DisaggregatedClusterReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
if allowed, err := dc.NamespaceScope.Allows(ctx, req.Namespace); err != nil || !allowed {
return ctrl.Result{}, err
}
var ddc dv1.DorisDisaggregatedCluster
err := dc.Get(ctx, req.NamespacedName, &ddc)
if apierrors.IsNotFound(err) {
Expand Down
9 changes: 9 additions & 0 deletions pkg/controller/doriscluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/apache/doris-operator/pkg/common/utils/k8s"
"github.com/apache/doris-operator/pkg/common/utils/resource"
"github.com/apache/doris-operator/pkg/common/utils/runtimeconfig"
"github.com/apache/doris-operator/pkg/controller/namespacescope"
"github.com/apache/doris-operator/pkg/controller/sub_controller"
"github.com/apache/doris-operator/pkg/controller/sub_controller/be"
bk "github.com/apache/doris-operator/pkg/controller/sub_controller/broker"
Expand Down Expand Up @@ -75,6 +76,7 @@ var (

// DorisClusterReconciler reconciles a DorisCluster object
type DorisClusterReconciler struct {
NamespaceScope *namespacescope.Scope
client.Client
Recorder record.EventRecorder
Scheme *runtime.Scheme
Expand Down Expand Up @@ -116,6 +118,9 @@ var (
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile
func (r *DorisClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if allowed, err := r.NamespaceScope.Allows(ctx, req.Namespace); err != nil || !allowed {
return ctrl.Result{}, err
}
klog.FromContext(ctx)
klog.Info("DorisClusterReconciler reconcile the update crd name ", req.Name, " namespace ", req.Namespace)
var edcr dorisv1.DorisCluster
Expand Down Expand Up @@ -358,6 +363,9 @@ func (r *DorisClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
builder := r.resourceBuilder(ctrl.NewControllerManagedBy(mgr))
builder = r.watchPodBuilder(builder)
builder = r.watchConfigMapBuilder(builder)
if r.NamespaceScope.Enabled() {
builder = namespacescope.Watch(builder, mgr.GetClient(), func() client.ObjectList { return &dorisv1.DorisClusterList{} })
}
return builder.Complete(r)
}

Expand All @@ -374,6 +382,7 @@ func (r *DorisClusterReconciler) Init(mgr ctrl.Manager, options *Options) {
subcs[brokerControllerName] = brk

if err := (&DorisClusterReconciler{
NamespaceScope: options.NamespaceScope,
Client: mgr.GetClient(),
Recorder: mgr.GetEventRecorderFor(name),
Scs: subcs,
Expand Down
97 changes: 97 additions & 0 deletions pkg/controller/namespacescope/scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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 namespacescope

import (
"context"
"fmt"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
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/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Scope filters reconciliation, not read permissions. The global cache remains
// available for explicit cross-namespace dependencies such as FDB ConfigMaps.
type Scope struct {
reader client.Reader
selector labels.Selector
}

func New(reader client.Reader, expression string) (*Scope, error) {
if expression == "" {
return nil, nil
}
selector, err := labels.Parse(expression)
if err != nil {
return nil, err
}
if selector.Empty() {
return nil, fmt.Errorf("namespace selector must not select every namespace")
}
return &Scope{reader: reader, selector: selector}, nil
}

func (s *Scope) Enabled() bool { return s != nil }

func (s *Scope) Allows(ctx context.Context, namespace string) (bool, error) {
if s == nil {
return true, nil
}
var ns corev1.Namespace
// Use the API reader: cached namespace labels must not retain ownership after
// an administrator removes the group label. Read failures never permit writes.
if err := s.reader.Get(ctx, types.NamespacedName{Name: namespace}, &ns); err != nil {
if apierrors.IsNotFound(err) {
return false, nil
}
return false, err
}
return s.selector.Matches(labels.Set(ns.Labels)), nil
}

// Watch also adopts existing CRs when a namespace joins the selected group.
func Watch(b *ctrl.Builder, c client.Client, list func() client.ObjectList) *ctrl.Builder {
return b.Watches(&corev1.Namespace{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, ns client.Object) []reconcile.Request {
objects := list()
if err := c.List(ctx, objects, client.InNamespace(ns.GetName())); err != nil {
ctrl.LoggerFrom(ctx).Error(err, "list namespace resources", "namespace", ns.GetName())
return nil
}
items, err := meta.ExtractList(objects)
if err != nil {
return nil
}
requests := make([]reconcile.Request, 0, len(items))
for _, item := range items {
obj, err := meta.Accessor(item)
if err == nil {
requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}})
}
}
return requests
}), builder.WithPredicates(predicate.LabelChangedPredicate{}))
}
Loading
Loading