Skip to content
Open
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
20 changes: 20 additions & 0 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"go.elastic.co/apm/v2"
"go.uber.org/automaxprocs/maxprocs"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
Expand Down Expand Up @@ -309,6 +310,11 @@ func Command() *cobra.Command {
nil,
"Comma-separated list of namespaces in which this operator should manage resources (defaults to all namespaces)",
)
cmd.Flags().String(
operator.NamespaceLabelSelectorFlag,
"",
"Label selector to filter namespaces that this operator should manage (in addition to namespace list). Format: key1=value1,key2=value2 or key1!=value1,key2!=value2. Empty means all namespaces.",
)
cmd.Flags().String(
operator.OperatorNamespaceFlag,
"",
Expand Down Expand Up @@ -713,6 +719,19 @@ func startOperator(ctx context.Context) error {
return err
}

// Parse namespace label selector if provided
var namespaceLabelSelector *metav1.LabelSelector
namespaceLabelSelectorStr := viper.GetString(operator.NamespaceLabelSelectorFlag)
if namespaceLabelSelectorStr != "" {
selector, err := metav1.ParseToLabelSelector(namespaceLabelSelectorStr)
if err != nil {
log.Error(err, "Failed to parse namespace label selector", "selector", namespaceLabelSelectorStr)
return err
}
namespaceLabelSelector = selector
log.Info("Namespace label selector configured", "selector", namespaceLabelSelector)
}

params := operator.Parameters{
Dialer: dialer,
ElasticsearchObservationInterval: viper.GetDuration(operator.ElasticsearchObservationIntervalFlag),
Expand All @@ -735,6 +754,7 @@ func startOperator(ctx context.Context) error {
SetDefaultSecurityContext: setDefaultSecurityContext,
ValidateStorageClass: viper.GetBool(operator.ValidateStorageClassFlag),
Tracer: tracer,
NamespaceLabelSelector: namespaceLabelSelector,
}

if viper.GetBool(operator.EnableWebhookFlag) {
Expand Down
41 changes: 41 additions & 0 deletions docs/design/0005-namespace-filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Namespace Filtering met Label Selectors

Deze ECK operator ondersteunt namespace filtering op basis van label selectors, waardoor je meerdere ECK operators kunt draaien die verschillende sets van namespaces beheren.

## Inleiding

Namespace filtering is een krachtige functie die je in staat stelt om de reikwijdte van je ECK operator te beperken tot specifieke namespaces binnen je Kubernetes cluster. Dit wordt bereikt door het toepassen van label selectors, die fungeren als een filtermechanisme om alleen die namespaces te selecteren die voldoen aan bepaalde criteria.

## Hoe het Werkt

De ECK operator maakt gebruik van Kubernetes label selectors om namespaces te filteren. Een label selector is een uitdrukking die bepaalt welke labels overeenkomen met de gespecificeerde criteria. Door deze selectors te gebruiken, kun je de ECK operator configureren om alleen die namespaces te beheren die de juiste labels hebben.

Bijvoorbeeld, als je een label selector hebt gedefinieerd als `environment: production`, dan zal de ECK operator alleen van toepassing zijn op die namespaces die het label `environment` hebben met de waarde `production`.

## Voordelen van Namespace Filtering

- **Gerichte Toepassingen**: Je kunt de ECK operator richten op specifieke namespaces, wat handig is in omgevingen waar meerdere teams of projecten dezelfde Kubernetes cluster delen.
- **Resource Optimalisatie**: Door de reikwijdte van de operator te beperken, worden de resources efficiënter gebruikt en wordt de kans op conflicten tussen verschillende toepassingen verminderd.
- **Veiligheid en Isolatie**: Namespace filtering helpt bij het handhaven van veiligheid en isolatie tussen verschillende delen van je applicatie of tussen verschillende applicaties die op dezelfde cluster draaien.

## Configuratie Voorbeeld

Hier is een voorbeeld van hoe je namespace filtering kunt configureren met behulp van label selectors in je ECK operator manifest:

```yaml
apiVersion: operator.elastic.co/v1
kind: Elasticsearch
metadata:
name: mijn-elk-cluster
namespace: mijn-namespace
labels:
environment: production
spec:
...
```

In dit voorbeeld zal de ECK operator alleen van toepassing zijn op de namespace `mijn-namespace` als deze het label `environment: production` heeft.

## Conclusie

Namespace filtering met label selectors biedt een flexibele en krachtige manier om de reikwijdte van je ECK operator te beheren. Door gebruik te maken van deze functie, kun je efficiënter werken met meerdere namespaces binnen je Kubernetes cluster en tegelijkertijd een hoge mate van isolatie en veiligheid handhaven.
4 changes: 2 additions & 2 deletions pkg/controller/agent/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ func (r *ReconcileAgent) Reconcile(ctx context.Context, request reconcile.Reques
return reconcile.Result{}, tracing.CaptureError(ctx, err)
}

if common.IsUnmanaged(ctx, agent) {
logconf.FromContext(ctx).Info("Object is currently not managed by this controller. Skipping reconciliation")
if common.IsUnmanagedOrFiltered(ctx, r.Client, agent, r.Parameters) {
logconf.FromContext(ctx).Info("Object is currently not managed by this controller or namespace is filtered. Skipping reconciliation")
return reconcile.Result{}, nil
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/apmserver/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ func (r *ReconcileApmServer) Reconcile(ctx context.Context, request reconcile.Re
return reconcile.Result{}, tracing.CaptureError(ctx, err)
}

if common.IsUnmanaged(ctx, &as) {
log.Info("Object currently not managed by this controller. Skipping reconciliation", "namespace", as.Namespace, "as_name", as.Name)
if common.IsUnmanagedOrFiltered(ctx, r.Client, &as, r.Parameters) {
log.Info("Object currently not managed by this controller or namespace is filtered. Skipping reconciliation", "namespace", as.Namespace, "as_name", as.Name)
return reconcile.Result{}, nil
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/association/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (

associatedKey := k8s.ExtractNamespacedName(associated)

if common.IsUnmanaged(ctx, associated) {
log.Info("Object is currently not managed by this controller. Skipping reconciliation")
if common.IsUnmanagedOrFiltered(ctx, r.Client, associated, r.Parameters) {
log.Info("Object is currently not managed by this controller or namespace is filtered. Skipping reconciliation")
return reconcile.Result{}, nil
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/autoscaling/elasticsearch/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ func (r *ReconcileElasticsearchAutoscaler) Reconcile(ctx context.Context, reques
return reconcile.Result{}, tracing.CaptureError(ctx, err)
}

if common.IsUnmanaged(ctx, &esa) {
msg := "Object is currently not managed by this controller. Skipping reconciliation"
if common.IsUnmanagedOrFiltered(ctx, r.Client, &esa, r.Parameters) {
msg := "Object is currently not managed by this controller or namespace is filtered. Skipping reconciliation"
log.Info(msg, "namespace", request.Namespace, "esa_name", request.Name)
return r.reportAsInactive(ctx, log, esa, msg)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/controller/beat/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ func (r *ReconcileBeat) Reconcile(ctx context.Context, request reconcile.Request
return reconcile.Result{}, tracing.CaptureError(ctx, err)
}

if common.IsUnmanaged(ctx, &beat) {
ulog.FromContext(ctx).Info("Object is currently not managed by this controller. Skipping reconciliation", "namespace", beat.Namespace, "beat_name", beat.Name)
if common.IsUnmanagedOrFiltered(ctx, r.Client, &beat, r.Parameters) {
ulog.FromContext(ctx).Info("Object is currently not managed by this controller or namespace is filtered. Skipping reconciliation", "namespace", beat.Namespace, "beat_name", beat.Name)
return reconcile.Result{}, nil
}

Expand Down
1 change: 1 addition & 0 deletions pkg/controller/common/operator/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
MetricsSecureFlag = "metrics-secure"
MetricsCertDirFlag = "metrics-cert-dir"
NamespacesFlag = "namespaces"
NamespaceLabelSelectorFlag = "namespace-label-selector"
OperatorNamespaceFlag = "operator-namespace"
SetDefaultSecurityContextFlag = "set-default-security-context"
TelemetryIntervalFlag = "telemetry-interval"
Expand Down
52 changes: 52 additions & 0 deletions pkg/controller/common/operator/namespace_filtering.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package operator

import (
"context"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"

ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
)

// ShouldManageNamespace determines if the operator should manage resources in the given namespace
// based on the configured namespace label selector.
func (p Parameters) ShouldManageNamespace(ctx context.Context, c client.Client, namespace string) (bool, error) {
// If no namespace label selector is configured, manage all namespaces (backwards compatibility)
if p.NamespaceLabelSelector == nil {
return true, nil
}

log := ulog.FromContext(ctx)

// Get the namespace object
var ns corev1.Namespace
if err := c.Get(ctx, client.ObjectKey{Name: namespace}, &ns); err != nil {
log.Error(err, "Failed to get namespace", "namespace", namespace)
return false, err
}

// Convert LabelSelector to labels.Selector
selector, err := metav1.LabelSelectorAsSelector(p.NamespaceLabelSelector)
if err != nil {
log.Error(err, "Failed to convert namespace label selector", "selector", p.NamespaceLabelSelector)
return false, err
}

// Check if namespace labels match the selector
matches := selector.Matches(labels.Set(ns.Labels))

log.V(1).Info("Namespace filtering check",
"namespace", namespace,
"labels", ns.Labels,
"selector", p.NamespaceLabelSelector,
"matches", matches)

return matches, nil
}
178 changes: 178 additions & 0 deletions pkg/controller/common/operator/namespace_filtering_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.

package operator

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
)

func TestShouldManageNamespace(t *testing.T) {
// Test namespace with labels
testNamespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-namespace",
Labels: map[string]string{
"env": "production",
"team": "platform",
},
},
}

// Another test namespace with different labels
devNamespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "dev-namespace",
Labels: map[string]string{
"env": "development",
"team": "platform",
},
},
}

// Namespace without relevant labels
otherNamespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "other-namespace",
Labels: map[string]string{
"purpose": "testing",
},
},
}

tests := []struct {
name string
namespaceLabelSelector *metav1.LabelSelector
namespace string
expectedResult bool
expectedError bool
}{
{
name: "No label selector - should manage all namespaces",
namespaceLabelSelector: nil,
namespace: "test-namespace",
expectedResult: true,
expectedError: false,
},
{
name: "Matching label selector - should manage namespace",
namespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"env": "production",
},
},
namespace: "test-namespace",
expectedResult: true,
expectedError: false,
},
{
name: "Non-matching label selector - should not manage namespace",
namespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"env": "production",
},
},
namespace: "dev-namespace",
expectedResult: false,
expectedError: false,
},
{
name: "Multiple label selectors - should match when all match",
namespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"env": "production",
"team": "platform",
},
},
namespace: "test-namespace",
expectedResult: true,
expectedError: false,
},
{
name: "Multiple label selectors - should not match when one doesn't match",
namespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"env": "production",
"team": "different",
},
},
namespace: "test-namespace",
expectedResult: false,
expectedError: false,
},
{
name: "Complex selector with matchExpressions",
namespaceLabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "env",
Operator: metav1.LabelSelectorOpNotIn,
Values: []string{"development", "staging"},
},
},
},
namespace: "test-namespace",
expectedResult: true,
expectedError: false,
},
{
name: "Label selector with missing labels",
namespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"env": "production",
},
},
namespace: "other-namespace",
expectedResult: false,
expectedError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fake client with test namespaces
fakeClient := k8s.NewFakeClient(testNamespace, devNamespace, otherNamespace)

// Create parameters with the test label selector
params := Parameters{
NamespaceLabelSelector: tt.namespaceLabelSelector,
}

// Test the function
result, err := params.ShouldManageNamespace(context.Background(), fakeClient, tt.namespace)

if tt.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedResult, result)
}
})
}
}

func TestShouldManageNamespace_NonExistentNamespace(t *testing.T) {
fakeClient := k8s.NewFakeClient()

params := Parameters{
NamespaceLabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"env": "production",
},
},
}

result, err := params.ShouldManageNamespace(context.Background(), fakeClient, "non-existent")

assert.False(t, result)
assert.Error(t, err)
}
Loading