Skip to content
Closed
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a430321
Namespace Management at Scale
moukoublen May 27, 2026
b3c60ae
license gate
moukoublen Jun 19, 2026
f8f0d38
fixes
moukoublen Jun 24, 2026
986195e
association fix
moukoublen Jun 24, 2026
8d97ca5
runnable fix
moukoublen Jun 25, 2026
a1e7701
simplify NamespaceFlipNotifier state
moukoublen Jun 25, 2026
ad98634
more e2e
moukoublen Jun 26, 2026
7456d50
fix e2e
moukoublen Jun 26, 2026
e77bba8
fixes
moukoublen Jun 26, 2026
2e7fd7e
Add timeout to subscriber channel send
moukoublen Jun 29, 2026
f72628b
add namespace FilterClient wrapper
moukoublen Jun 30, 2026
b468d5c
fix trial controller scope
moukoublen Jun 30, 2026
14dab40
renames
moukoublen Jun 30, 2026
7563b65
extend namespace matcher with direct cache usage receiver methods; ma…
moukoublen Jun 30, 2026
28a596f
add watches.NamespacedKind to pkg/controller/license/license_controll…
moukoublen Jun 30, 2026
939dfe9
fix: use cache.Cahce in WatchNamespaceFlips
moukoublen Jun 30, 2026
d431072
comment on namespace delete
moukoublen Jun 30, 2026
71c06f1
comment on elasticsearch controller onNamespaceOutOfScope
moukoublen Jun 30, 2026
d12deb1
remove mock client
moukoublen Jul 1, 2026
6b5cf4d
webhook fix
moukoublen Jul 1, 2026
be069b3
comments
moukoublen Jul 1, 2026
cb86931
fix integration test
moukoublen Jul 1, 2026
0ae11b0
fix namepsace controller; use cache directly
moukoublen Jul 1, 2026
16f2bfe
Fix association mappin on namespace state change
moukoublen Jul 2, 2026
e615a77
lint fixes
moukoublen Jul 2, 2026
8b03fd7
improve comments
moukoublen Jul 2, 2026
2a4deef
fix license controller
moukoublen Jul 3, 2026
eba17db
fix integration test license_controller_integration_test.go
moukoublen Jul 3, 2026
a081482
use slices.DeleteFunc
moukoublen Jul 3, 2026
9a97bec
fix remote cluster re-queue
moukoublen Jul 3, 2026
5c52892
fix scp namespace state change
moukoublen Jul 3, 2026
92ab9d8
fix after rebase
moukoublen Jul 3, 2026
f614f7f
add test helper for namespace labels
moukoublen Jul 3, 2026
772ea76
fix autoops namespaceFlipRequests
moukoublen Jul 3, 2026
3e50487
change filter client Get delegation
moukoublen Jul 3, 2026
25448fb
Fix NamespaceMatcher MatchingNamespacesFromCache
moukoublen Jul 3, 2026
eba77b3
fix comments
moukoublen Jul 3, 2026
6af77b7
add r.NamespaceMatcher.Matches(request.Namespace) in pkg/controller/l…
moukoublen Jul 6, 2026
8a9157a
fix onNamespaceOutOfScope of logstash controller
moukoublen Jul 6, 2026
afc2025
Log backpressure on blocked subscriber sends instead of timing out
moukoublen Jul 6, 2026
15121d2
Make namespace controller and the seed runname running on all replica…
moukoublen Jul 6, 2026
c07a9d3
clean up notes
moukoublen Jul 6, 2026
8873c81
Use the in-memory namespace match-state map instead of live cache lab…
moukoublen Jul 7, 2026
07ed416
cleanup NamespaceMatcher
moukoublen Jul 7, 2026
4da2395
Add InitialSeed and WaitForInitialSeeding in the namespace matcher
moukoublen Jul 7, 2026
c4c2434
reuse namespaces in pkg/telemetry/telemetry.go
moukoublen Jul 7, 2026
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
788 changes: 394 additions & 394 deletions NOTICE.txt

Large diffs are not rendered by default.

76 changes: 72 additions & 4 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/selection"
Expand All @@ -44,6 +45,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
crwebhook "sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/yaml"

"github.com/elastic/cloud-on-k8s/v3/pkg/about"
agentv1alpha1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/agent/v1alpha1"
Expand All @@ -69,6 +71,7 @@ import (
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/container"
commonesclient "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/esclient"
commonlicense "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/license"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/nsmatch"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/operator"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/password"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/reconciler"
Expand All @@ -86,6 +89,7 @@ import (
licensetrial "github.com/elastic/cloud-on-k8s/v3/pkg/controller/license/trial"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/logstash"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/maps"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/namespace"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/packageregistry"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/remotecluster"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/stackconfigpolicy"
Expand Down Expand Up @@ -593,7 +597,29 @@ func startOperator(ctx context.Context) error {
log.Error(err, "Failed to parse managed namespaces flag")
return err
}

// Parse the optional namespace label selector. This field is config-file
// only (a metav1.LabelSelector object); it has no CLI flag. When set,
// the operator runs in dynamic mode: the cache is cluster-wide and a
// Matcher consulted by a predicate on every controller's watches
// evaluates the selector against Namespace labels per event.
parsedNsSelector, err := parseNSSelector(log)
if err != nil {
return err
}
dynamicNSSelector := parsedNsSelector != nil

// Validate that dynamic ns selector cannot co-exist with the fixed list of managed namespaces.
if dynamicNSSelector && len(managedNamespaces) > 0 {
log.Error(errors.New("namespaces and namespaceSelector are mutually exclusive"), "Invalid configuration")
return errors.New("namespaces and namespaceSelector are mutually exclusive")
}

switch {
case dynamicNSSelector:
log.Info("Operator configured to manage namespaces dynamically via selector",
"selector", parsedNsSelector.String(),
"operator_namespace", operatorNamespace)
case len(managedNamespaces) == 0:
log.Info("Operator configured to manage all namespaces")
case len(managedNamespaces) == 1 && managedNamespaces[0] == operatorNamespace:
Expand All @@ -611,13 +637,26 @@ func startOperator(ctx context.Context) error {
return err
}

namespaceMatcher := nsmatch.NewNamespaceMatcher(parsedNsSelector, operatorNamespace)

opts.Cache = cache.Options{
DefaultNamespaces: map[string]cache.Config{},
DefaultTransform: cache.TransformStripManagedFields(),
ByObject: byObject,
}
for _, ns := range managedNamespaces {
opts.Cache.DefaultNamespaces[ns] = cache.Config{}
if !dynamicNSSelector {
Comment thread
moukoublen marked this conversation as resolved.
for _, ns := range managedNamespaces {
opts.Cache.DefaultNamespaces[ns] = cache.Config{}
}
} else {
opts.NewClient = func(config *rest.Config, options client.Options) (client.Client, error) {
delegate, err := client.New(config, options) // cache-backed; options.Cache already set by mgr
if err != nil {
return nil, err
}

return nsmatch.NewFilterClient(delegate, namespaceMatcher), nil
}
}

// only expose prometheus metrics if provided a non-zero port
Expand Down Expand Up @@ -651,6 +690,9 @@ func startOperator(ctx context.Context) error {
return err
}

// cache can only be set after manager is created.
namespaceMatcher.SetCache(mgr.GetCache())

// Retrieve globally shared CA if any
ca, err := readOptionalCA(viper.GetString(operator.CADirFlag))
if err != nil {
Expand Down Expand Up @@ -752,6 +794,7 @@ func startOperator(ctx context.Context) error {
SetDefaultSecurityContext: setDefaultSecurityContext,
ValidateStorageClass: viper.GetBool(operator.ValidateStorageClassFlag),
Tracer: tracer,
NamespaceMatcher: namespaceMatcher,
}

if viper.GetBool(operator.EnableWebhookFlag) {
Expand All @@ -773,7 +816,7 @@ func startOperator(ctx context.Context) error {

disableTelemetry := viper.GetBool(operator.DisableTelemetryFlag)
telemetryInterval := viper.GetDuration(operator.TelemetryIntervalFlag)
go asyncTasks(ctx, mgr, cfg, managedNamespaces, operatorNamespace, operatorInfo, disableTelemetry, telemetryInterval, tracer, dialer)
go asyncTasks(ctx, mgr, cfg, managedNamespaces, operatorNamespace, namespaceMatcher, operatorInfo, disableTelemetry, telemetryInterval, tracer, dialer)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] User-secret GC ignores namespaceSelector in dynamic mode

managedNamespaces is empty in dynamic mode (it is only populated in static mode). asyncTasks passes it directly to garbageCollectUsers (line 911), which calls association.NewUsersGarbageCollector(cfg, managedNamespaces). That constructor explicitly converts an empty list to all-namespaces (gc.go:54-55):

if len(managedNamespaces) == 0 {
    managedNamespaces = []string{AllNamespaces}
}

This means a selector-scoped operator will scan and potentially delete association user secrets across the entire cluster, including namespaces it is explicitly not configured to manage.

The fix needs care: namespaceMatcher.MatchingNamespaces() returns the in-memory set, but asyncTasks runs while the seeder goroutine may still be in progress, so the map could be incomplete at GC time. The safest option is to pass namespaceMatcher into asyncTasks and do a direct API namespace list with the configured selector at GC time, rather than relying on the in-memory map.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the autoops garbadge collector I think.

Regarding the solution, if we are about to use the direct API, then perhaps pre-seeding using the direct API in main is safer?

if dynamicNSSelector {
	nsList, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
	if err != nil {
		log.Error(err, "Failed to list namespaces to seed the namespace matcher")
		return err
	}
	for i := range nsList.Items {
		ns := &nsList.Items[i]
		namespaceMatcher.ObserveNamespace(ns)
	}
}

(I had already tested that when exploring pre-seeding) This way we will have the state ready in the same resource (namespace matcher)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, a receiver function like WaitForCacheSync(ctx) for nsmatcher called WaitForInitialSeeding(ctx) that unblocks when the runner is done

And call this function inside the the async task after the WaitForCacheSync(ctx)

I think this is simpler I would go with that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4da2395


log.Info("Starting the manager", "uuid", operatorInfo.OperatorUUID,
"namespace", operatorNamespace, "version", operatorInfo.BuildInfo.Version,
Expand Down Expand Up @@ -828,6 +871,7 @@ func asyncTasks(
cfg *rest.Config,
managedNamespaces []string,
operatorNamespace string,
namespaceMatcher *nsmatch.NamespaceMatcher,
operatorInfo about.OperatorInfo,
disableTelemetry bool,
telemetryInterval time.Duration,
Expand All @@ -851,7 +895,7 @@ func asyncTasks(
if !disableTelemetry {
// Start the telemetry reporter
go func() {
tr := telemetry.NewReporter(operatorInfo, mgr.GetClient(), operatorNamespace, managedNamespaces, telemetryInterval, tracer)
tr := telemetry.NewReporter(operatorInfo, mgr.GetClient(), operatorNamespace, managedNamespaces, namespaceMatcher, telemetryInterval, tracer)
tr.Start(ctx)
}()
}
Expand Down Expand Up @@ -927,6 +971,7 @@ func registerControllers(mgr manager.Manager, params operator.Parameters, access
name string
registerFunc func(manager.Manager, operator.Parameters) error
}{
{name: "Namespace", registerFunc: namespace.Add},
{name: "APMServer", registerFunc: apmserver.Add},
{name: "Elasticsearch", registerFunc: elasticsearch.Add},
{name: "ElasticsearchAutoscaling", registerFunc: autoscaling.Add},
Expand Down Expand Up @@ -1101,3 +1146,26 @@ func buildByObject(restrictWatchedResources bool) (map[client.Object]cache.ByObj

return byObject, nil
}

func parseNSSelector(log logr.Logger) (labels.Selector, error) {
if !viper.IsSet(operator.NamespaceSelectorFlag) {
return nil, nil
}
raw := viper.Get(operator.NamespaceSelectorFlag)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe register this flag properly with an explanation so that it becomes better discoverable and is mentioned in help output and docs:

cmd.Flags().String(
    operator.NamespaceSelectorFlag,
    "",
    "Label selector for dynamic namespace management (enterprise feature). "+
        "Must be set via config file as a metav1.LabelSelector object "+
        "(e.g. matchLabels: {eck-managed: 'true'}). "+
        "Mutually exclusive with --namespaces.",
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention was not to have this as a CLI flag and only as part of the YAML file. Do you think we should add it as a CLI flag as well?

yamlBytes, err := yaml.Marshal(raw)
if err != nil {
log.Error(err, "Failed to serialize namespace-selector for parsing")
return nil, err
}
var ls metav1.LabelSelector
if err := yaml.Unmarshal(yamlBytes, &ls); err != nil {
log.Error(err, "Failed to parse namespace-selector")
return nil, err
}
sel, err := metav1.LabelSelectorAsSelector(&ls)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] Empty selector becomes match-all. metav1.LabelSelectorAsSelector on an empty LabelSelector{} returns labels.Everything(), so namespace-selector: {} in the operator config silently means "manage all namespaces dynamically" rather than "disabled." The Helm chart correctly guards this (with/empty skip {}), but a hand-written operator config wouldn't. Consider treating an empty selector as disabled (return nil) or documenting the behavior explicitly.

if err != nil {
log.Error(err, "Invalid namespace-selector")
return nil, err
}
return sel, nil
}
40 changes: 21 additions & 19 deletions cmd/manager/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
esavalidation "github.com/elastic/cloud-on-k8s/v3/pkg/controller/autoscaling/elasticsearch/validation"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/certificates"
commonlicense "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/license"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/nsmatch"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/operator"
"github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/tracing"
commonwebhook "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/webhook"
Expand Down Expand Up @@ -79,34 +80,35 @@ func setupWebhook(
) {
manageWebhookCerts := viper.GetBool(operator.ManageWebhookCertsFlag)
if manageWebhookCerts {
if err := reconcileWebhookCertsAndAddController(ctx, mgr, params.CertRotation, clientset, tracer); err != nil {
if err := reconcileWebhookCertsAndAddController(ctx, mgr, params.CertRotation, params.NamespaceMatcher, clientset, tracer); err != nil {
log.Error(err, "unable to setup the webhook certificates")
os.Exit(1)
}
}

checker := commonlicense.NewLicenseChecker(mgr.GetClient(), params.OperatorNamespace)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Webhook license checks can still depend on unseeded matcher state on standby replicas. The namespace-scope part now uses MatchesCachedLabels, which fixes the earlier skip-validation issue, but this checker is built from mgr.GetClient(). In dynamic mode that client is the FilterClient, and license discovery does a cluster-wide Secret list that is filtered through the in-memory NamespaceMatcher.Matches state.

Because the namespace controller/seeder are leader-election gated, non-leader replicas that still serve webhook traffic do not keep that in-memory state populated. If the valid enterprise license Secret is in a dynamically selected namespace rather than the always-managed operator namespace, a standby webhook can fail to see it and reject enterprise-gated resources even though the request namespace itself matched via cached labels.

Can the webhook license checker avoid the state-based FilterClient path here, or should we explicitly require/enforce operator licenses to be read only from the operator namespace in namespace-selector mode?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we change the client to a wide one (e.g. cache) then the license check might fetch license secrets that it should not.

I think a proper fix for that is to enhance the license checker itself a bit. Before we move with enforcing the operator license to live in the operator namespace, let me think a bit about any possible enhancement, and I will come back.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in this 15121d2

matcher := params.NamespaceMatcher
// setup webhooks for supported types
commonwebhook.RegisterResourceWebhook(mgr, apmv1.WebhookPath, checker, managedNamespaces, apmv1.Validate, "APM Server")
commonwebhook.RegisterResourceWebhook(mgr, apmv1beta1.WebhookPath, checker, managedNamespaces, apmv1beta1.Validate, "APM Server")
commonwebhook.RegisterResourceWebhook(mgr, beatv1beta1.WebhookPath, checker, managedNamespaces, beatv1beta1.Validate, "Beat")
commonwebhook.RegisterResourceWebhook(mgr, entv1.WebhookPath, checker, managedNamespaces, entv1.Validate, "Enterprise Search")
commonwebhook.RegisterResourceWebhook(mgr, entv1beta1.WebhookPath, checker, managedNamespaces, entv1beta1.Validate, "Enterprise Search")
commonwebhook.RegisterResourceWebhook(mgr, esv1beta1.WebhookPath, checker, managedNamespaces, esv1beta1.Validate, "Elasticsearch")
commonwebhook.RegisterResourceWebhook(mgr, kbv1.WebhookPath, checker, managedNamespaces, kbv1.Validate, "Kibana")
commonwebhook.RegisterResourceWebhook(mgr, kbv1beta1.WebhookPath, checker, managedNamespaces, kbv1beta1.Validate, "Kibana")
commonwebhook.RegisterResourceWebhook(mgr, emsv1alpha1.WebhookPath, checker, managedNamespaces, emsv1alpha1.Validate, "Elastic Maps Server")
commonwebhook.RegisterResourceWebhook(mgr, eprv1alpha1.WebhookPath, checker, managedNamespaces, eprv1alpha1.Validate, "Package Registry")
scpvalidation.RegisterWebhook(mgr, checker, managedNamespaces, params.OperatorNamespace)
commonwebhook.RegisterResourceWebhook(mgr, apmv1.WebhookPath, checker, managedNamespaces, matcher, apmv1.Validate, "APM Server")
commonwebhook.RegisterResourceWebhook(mgr, apmv1beta1.WebhookPath, checker, managedNamespaces, matcher, apmv1beta1.Validate, "APM Server")
commonwebhook.RegisterResourceWebhook(mgr, beatv1beta1.WebhookPath, checker, managedNamespaces, matcher, beatv1beta1.Validate, "Beat")
commonwebhook.RegisterResourceWebhook(mgr, entv1.WebhookPath, checker, managedNamespaces, matcher, entv1.Validate, "Enterprise Search")
commonwebhook.RegisterResourceWebhook(mgr, entv1beta1.WebhookPath, checker, managedNamespaces, matcher, entv1beta1.Validate, "Enterprise Search")
commonwebhook.RegisterResourceWebhook(mgr, esv1beta1.WebhookPath, checker, managedNamespaces, matcher, esv1beta1.Validate, "Elasticsearch")
commonwebhook.RegisterResourceWebhook(mgr, kbv1.WebhookPath, checker, managedNamespaces, matcher, kbv1.Validate, "Kibana")
commonwebhook.RegisterResourceWebhook(mgr, kbv1beta1.WebhookPath, checker, managedNamespaces, matcher, kbv1beta1.Validate, "Kibana")
commonwebhook.RegisterResourceWebhook(mgr, emsv1alpha1.WebhookPath, checker, managedNamespaces, matcher, emsv1alpha1.Validate, "Elastic Maps Server")
commonwebhook.RegisterResourceWebhook(mgr, eprv1alpha1.WebhookPath, checker, managedNamespaces, matcher, eprv1alpha1.Validate, "Package Registry")
scpvalidation.RegisterWebhook(mgr, checker, managedNamespaces, params.OperatorNamespace, matcher)

// Logstash, Elasticsearch v1, ElasticsearchAutoscaling, and AutoOps validating webhooks are wired up
// separately so their validators can use the API client and/or embed license checks. Elasticsearch
// v1beta1 remains in the RegisterResourceWebhook list above because it only needs a ValidateFunc.
esvalidation.RegisterWebhook(mgr, params.ValidateStorageClass, exposedNodeLabels, checker, managedNamespaces)
esavalidation.RegisterWebhook(mgr, params.ValidateStorageClass, checker, managedNamespaces)
lsvalidation.RegisterWebhook(mgr, params.ValidateStorageClass, managedNamespaces)
autoopsvalidation.RegisterWebhook(mgr, checker, managedNamespaces)
agentcontroller.RegisterWebhook(mgr, checker, managedNamespaces)
esvalidation.RegisterWebhook(mgr, params.ValidateStorageClass, exposedNodeLabels, checker, managedNamespaces, matcher)
esavalidation.RegisterWebhook(mgr, params.ValidateStorageClass, checker, managedNamespaces, matcher)
lsvalidation.RegisterWebhook(mgr, params.ValidateStorageClass, managedNamespaces, matcher)
autoopsvalidation.RegisterWebhook(mgr, checker, managedNamespaces, matcher)
agentcontroller.RegisterWebhook(mgr, checker, managedNamespaces, matcher)

// wait for the secret to be populated in the local filesystem before returning
interval := time.Second * 1
Expand Down Expand Up @@ -134,7 +136,7 @@ func setupWebhook(
}
}

func reconcileWebhookCertsAndAddController(ctx context.Context, mgr manager.Manager, certRotation certificates.RotationParams, clientset kubernetes.Interface, tracer *apm.Tracer) error {
func reconcileWebhookCertsAndAddController(ctx context.Context, mgr manager.Manager, certRotation certificates.RotationParams, m *nsmatch.NamespaceMatcher, clientset kubernetes.Interface, tracer *apm.Tracer) error {
ctx = tracing.NewContextTransaction(ctx, tracer, tracing.ReconciliationTxType, webhook.ControllerName, nil)
defer tracing.EndContextTransaction(ctx)
log.Info("Automatic management of the webhook certificates enabled")
Expand All @@ -157,5 +159,5 @@ func reconcileWebhookCertsAndAddController(ctx context.Context, mgr manager.Mana
return err
}

return webhook.Add(mgr, webhookParams, clientset, wh, tracer)
return webhook.Add(mgr, m, webhookParams, clientset, wh, tracer)
}
4 changes: 4 additions & 0 deletions deploy/eck-operator/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ data:
{{- with .Values.managedNamespaces }}
namespaces: [{{ join "," . }}]
{{- end }}
{{- with .Values.namespaceSelector }}
namespace-selector:
{{- . | toYaml | nindent 6 }}
{{- end }}
operator-namespace: {{ .Release.Namespace }}
enable-leader-election: {{ .Values.config.enableLeaderElection }}
elasticsearch-observation-interval: {{ .Values.config.elasticsearchObservationInterval }}
Expand Down
10 changes: 10 additions & 0 deletions deploy/eck-operator/templates/validate-chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@
{{- fail "Leader election must be enabled with more than one replica" -}}
{{- end -}}
{{- end -}}

{{- if not (empty .Values.namespaceSelector) -}}
{{- if not (empty .Values.managedNamespaces) -}}
{{- fail "namespaceSelector and managedNamespaces are mutually exclusive" -}}
{{- end -}}

{{- if not .Values.createClusterScopedResources -}}
{{- fail "namespaceSelector requires createClusterScopedResources to be true" -}}
{{- end -}}
{{- end -}}
17 changes: 17 additions & 0 deletions deploy/eck-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,25 @@ nameOverride: "elastic-operator"
fullnameOverride: "elastic-operator"

# managedNamespaces is the set of namespaces that the operator manages. Leave empty to manage all namespaces.
# Mutually exclusive with namespaceSelector.
managedNamespaces: []

# namespaceSelector is a Kubernetes label selector that dynamically controls which namespaces the operator manages.
# The operator watches all namespaces cluster-wide and filters events by matching each namespace's labels against
# this selector at runtime. When a namespace gains or loses the matching labels the operator automatically
# starts or stops managing resources in that namespace without a restart.
# Mutually exclusive with managedNamespaces. Requires createClusterScopedResources: true.
# Example:
# namespaceSelector:
# matchLabels:
# eck-managed: "true"
# namespaceSelector:
# matchExpressions:
# - key: environment
# operator: In
# values: [production, staging]
namespaceSelector: {}

# installCRDs determines whether Custom Resource Definitions (CRD) are installed by the chart.
# Note that CRDs are global resources and require cluster admin privileges to install.
# If you are sharing a cluster with other users who may want to install ECK on their own namespaces, setting this to true can have unintended consequences.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/third-party-dependencies/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ This page lists the third-party dependencies used to build {{eck}} from the main
| [k8s.io/utils](https://github.com/kubernetes/utils) | v0.0.0-20260319190234-28399d86e0b5 | Apache-2.0 |
| [sigs.k8s.io/controller-runtime](https://sigs.k8s.io/controller-runtime) | v0.24.1 | Apache-2.0 |
| [sigs.k8s.io/controller-tools](https://sigs.k8s.io/controller-tools) | v0.21.0 | Apache-2.0 |
| [sigs.k8s.io/yaml](https://sigs.k8s.io/yaml) | v1.6.0 | Apache-2.0 |


## Indirect dependencies [k8s-dependencies-indirect]
Expand Down Expand Up @@ -274,5 +275,4 @@ This page lists the third-party dependencies used to build {{eck}} from the main
| [sigs.k8s.io/json](https://sigs.k8s.io/json) | v0.0.0-20250730193827-2d320260d730 | Apache-2.0 |
| [sigs.k8s.io/randfill](https://sigs.k8s.io/randfill) | v1.0.0 | Apache-2.0 |
| [sigs.k8s.io/structured-merge-diff/v6](https://sigs.k8s.io/structured-merge-diff/v6) | v6.4.0 | Apache-2.0 |
| [sigs.k8s.io/yaml](https://sigs.k8s.io/yaml) | v1.6.0 | Apache-2.0 |

3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ require (
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/controller-tools v0.21.0
sigs.k8s.io/yaml v1.6.0
)

require (
Expand Down Expand Up @@ -170,6 +171,7 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
Expand Down Expand Up @@ -223,7 +225,6 @@ require (
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

// both of these dependencies are used by vegeta, but the version they use is older and did not include a licence. we require the licence and so pin both of these
Expand Down
Loading
Loading