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
60 changes: 60 additions & 0 deletions docs/docs/operator/building-blocks/controllers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,66 @@ public async Task<ReconciliationResult<V1DemoEntity>> ReconcileAsync(
- Avoid long-running operations in the reconciliation loop
- Use background tasks for time-consuming operations

## Filtering Watched Resources

By default, a controller's resource watcher observes all resources of the given entity type
cluster-wide. If `OperatorSettings.Namespace` is configured, the watcher is automatically
scoped to that namespace instead.
You can further restrict the watched set using **label selectors** and **field selectors**.

### Label Selectors

Implement `IEntityLabelSelector<TEntity>` to filter resources by label expressions.
See the [Kubernetes Label Selectors documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for the full syntax reference.
Register it when adding the controller:

```csharp
public class MyDemoEntityLabelSelector : IEntityLabelSelector<V1DemoEntity>
{
public ValueTask<string?> GetLabelSelectorAsync(CancellationToken cancellationToken) =>
ValueTask.FromResult<string?>(new EqualsLabelSelector("app", "my-operator"));
}

// Registration
builder.Services.AddKubernetesOperator()
.AddControllerWithLabelSelector<V1DemoEntityController, V1DemoEntity, MyDemoEntityLabelSelector>();
```

When no label selector is needed, the built-in `DefaultEntityLabelSelector<TEntity>` returns `null`
(no filtering).

### Field Selectors

Kubernetes field selectors filter by resource fields (e.g. `metadata.name`, `metadata.namespace`).
They support `=` / `==` (equality) and `!=` (inequality) operators, and only a small subset of
fields per resource type.
See the [Kubernetes Field Selectors documentation](https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/) for the full syntax reference.

Implement `IEntityFieldSelector<TEntity>` and register it when adding the controller:

```csharp
public class MyDemoEntityFieldSelector : IEntityFieldSelector<V1DemoEntity>
{
public ValueTask<string?> GetFieldSelectorAsync(CancellationToken cancellationToken) =>
ValueTask.FromResult<string?>(new EqualsFieldSelector("metadata.name", "my-demo-entity"));
}

// Registration
builder.Services.AddKubernetesOperator()
.AddControllerWithFieldSelector<V1DemoEntityController, V1DemoEntity, MyDemoEntityFieldSelector>();
```

When no field selector is needed, the built-in `DefaultEntityFieldSelector<TEntity>` returns `null`
(no filtering).

### Summary of Registration Methods

| Method | Label Selector | Field Selector |
|---|---|---|
| `AddController<TImpl, TEntity>()` | Default (none) | Default (none) |
| `AddControllerWithLabelSelector<TImpl, TEntity, TLabel>()` | Custom | Default (none) |
| `AddControllerWithFieldSelector<TImpl, TEntity, TField>()` | Default (none) | Custom |

## Common Pitfalls

1. **Infinite Loops**: Avoid creating reconciliation loops that trigger themselves
Expand Down
19 changes: 17 additions & 2 deletions src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,33 @@ IOperatorBuilder AddController<TImplementation, TEntity>()
where TEntity : IKubernetesObject<V1ObjectMeta>;

/// <summary>
/// Add a controller implementation for a specific entity to the operator.
/// Add a controller implementation for a specific entity to the operator,
/// with a custom label selector.
/// The metadata for the entity must be added as well.
/// </summary>
/// <typeparam name="TImplementation">Implementation type of the controller.</typeparam>
/// <typeparam name="TEntity">Entity type.</typeparam>
/// <typeparam name="TLabelSelector">Label Selector type.</typeparam>
/// <returns>The builder for chaining.</returns>
IOperatorBuilder AddController<TImplementation, TEntity, TLabelSelector>()
IOperatorBuilder AddControllerWithLabelSelector<TImplementation, TEntity, TLabelSelector>()
where TImplementation : class, IEntityController<TEntity>
where TEntity : IKubernetesObject<V1ObjectMeta>
where TLabelSelector : class, IEntityLabelSelector<TEntity>;

/// <summary>
/// Add a controller implementation for a specific entity to the operator,
/// with a custom field selector.
/// The metadata for the entity must be added as well.
/// </summary>
/// <typeparam name="TImplementation">Implementation type of the controller.</typeparam>
/// <typeparam name="TEntity">Entity type.</typeparam>
/// <typeparam name="TFieldSelector">Field Selector type.</typeparam>
/// <returns>The builder for chaining.</returns>
IOperatorBuilder AddControllerWithFieldSelector<TImplementation, TEntity, TFieldSelector>()
where TImplementation : class, IEntityController<TEntity>
where TEntity : IKubernetesObject<V1ObjectMeta>
where TFieldSelector : class, IEntityFieldSelector<TEntity>;

/// <summary>
/// Add a finalizer implementation for a specific entity.
/// This adds the implementation as a transient service and registers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using k8s;
using k8s.Models;

namespace KubeOps.Abstractions.Entities;

public sealed class DefaultEntityFieldSelector<TEntity> : IEntityFieldSelector<TEntity>
where TEntity : IKubernetesObject<V1ObjectMeta>
{
public ValueTask<string?> GetFieldSelectorAsync(CancellationToken cancellationToken) =>
ValueTask.FromResult<string?>(null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

namespace KubeOps.Abstractions.Entities;

public class DefaultEntityLabelSelector<TEntity> : IEntityLabelSelector<TEntity>
public sealed class DefaultEntityLabelSelector<TEntity> : IEntityLabelSelector<TEntity>
where TEntity : IKubernetesObject<V1ObjectMeta>
{
public ValueTask<string?> GetLabelSelectorAsync(CancellationToken cancellationToken) => ValueTask.FromResult<string?>(null);
public ValueTask<string?> GetLabelSelectorAsync(CancellationToken cancellationToken) =>
ValueTask.FromResult<string?>(null);
}
19 changes: 19 additions & 0 deletions src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using k8s;
using k8s.Models;

namespace KubeOps.Abstractions.Entities;

// This is the same pattern used by Microsoft on ILogger<T>.
// An alternative would be to use a KeyedSingleton when registering this however that's only valid from .NET 8 and above.
// Other methods are far less elegant
#pragma warning disable S2326
public interface IEntityFieldSelector<TEntity>
where TEntity : IKubernetesObject<V1ObjectMeta>
{
ValueTask<string?> GetFieldSelectorAsync(CancellationToken cancellationToken);
}
#pragma warning restore S2326
4 changes: 3 additions & 1 deletion src/KubeOps.KubernetesClient/IKubernetesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using k8s.Models;

using KubeOps.Abstractions.Entities;
using KubeOps.KubernetesClient.LabelSelectors;
using KubeOps.KubernetesClient.Selectors;

namespace KubeOps.KubernetesClient;

Expand Down Expand Up @@ -740,6 +740,7 @@ Watcher<TEntity> Watch<TEntity>(
/// Defaults to changes from the beginning of history.
/// </param>
/// <param name="labelSelector">A string, representing an optional label selector for filtering watched objects.</param>
/// <param name="fieldSelector">A string, representing an optional field selector for filtering watched objects.</param>
/// <param name="allowWatchBookmarks">
/// Parameter to tell the server to send BOOKMARK events. However, if the server has no implementation or
/// configuration for bookmarks, this flag is ignored.
Expand All @@ -751,6 +752,7 @@ Watcher<TEntity> Watch<TEntity>(
string? @namespace = null,
string? resourceVersion = null,
string? labelSelector = null,
string? fieldSelector = null,
bool? allowWatchBookmarks = null,
CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta>;
Expand Down
3 changes: 3 additions & 0 deletions src/KubeOps.KubernetesClient/KubernetesClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ public Watcher<TEntity> Watch<TEntity>(
string? @namespace = null,
string? resourceVersion = null,
string? labelSelector = null,
string? fieldSelector = null,
bool? allowWatchBookmarks = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
where TEntity : IKubernetesObject<V1ObjectMeta>
Expand All @@ -452,6 +453,7 @@ public Watcher<TEntity> Watch<TEntity>(
@namespace,
metadata.PluralName,
allowWatchBookmarks: allowWatchBookmarks,
fieldSelector: fieldSelector,
labelSelector: labelSelector,
resourceVersion: resourceVersion,
watch: true,
Expand All @@ -461,6 +463,7 @@ public Watcher<TEntity> Watch<TEntity>(
metadata.Version,
metadata.PluralName,
allowWatchBookmarks: allowWatchBookmarks,
fieldSelector: fieldSelector,
labelSelector: labelSelector,
resourceVersion: resourceVersion,
watch: true,
Expand Down
24 changes: 0 additions & 24 deletions src/KubeOps.KubernetesClient/LabelSelectors/LabelSelector.cs

This file was deleted.

16 changes: 16 additions & 0 deletions src/KubeOps.KubernetesClient/Selectors/EqualsFieldSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Field-selector that checks if a certain field equals a specific value.
/// Produces the expression <c>field=value</c>.
/// </summary>
/// <param name="Field">The field path (e.g. <c>metadata.name</c>).</param>
/// <param name="Value">The required value.</param>
public record EqualsFieldSelector(string Field, string Value) : FieldSelector
{
protected override string ToExpression() => $"{Field}={Value}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.LabelSelectors;
namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Label-selector that checks if a certain label contains
Expand All @@ -11,7 +11,7 @@ namespace KubeOps.KubernetesClient.LabelSelectors;
/// </summary>
/// <param name="Label">The label that needs to equal to one of the values.</param>
/// <param name="Values">The possible values.</param>
public record EqualsSelector(string Label, params string[] Values) : LabelSelector
public record EqualsLabelSelector(string Label, params string[] Values) : LabelSelector
{
protected override string ToExpression() => $"{Label} in ({string.Join(",", Values)})";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.LabelSelectors;
namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Selector that checks if a certain label exists.
/// </summary>
/// <param name="Label">The label that needs to exist on the entity/resource.</param>
public record ExistsSelector(string Label) : LabelSelector
public record ExistsLabelSelector(string Label) : LabelSelector
{
protected override string ToExpression() => $"{Label}";
}
13 changes: 13 additions & 0 deletions src/KubeOps.KubernetesClient/Selectors/FieldSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Different field selectors for querying the Kubernetes API.
/// </summary>
/// <seealso href="https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/">Kubernetes Field Selectors</seealso>
#pragma warning disable S2094
public abstract record FieldSelector : KubernetesSelector;
#pragma warning restore S2094
24 changes: 24 additions & 0 deletions src/KubeOps.KubernetesClient/Selectors/KubernetesSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Common base record for all Kubernetes selector types (label and field selectors).
/// </summary>
public abstract record KubernetesSelector
{
/// <summary>
/// Cast the selector to a string expression.
/// </summary>
/// <param name="selector">The selector.</param>
/// <returns>A string representation of the selector.</returns>
public static implicit operator string(KubernetesSelector selector) => selector.ToExpression();

/// <summary>
/// Create an expression string from the selector.
/// </summary>
/// <returns>A string that represents the selector expression.</returns>
protected abstract string ToExpression();
}
13 changes: 13 additions & 0 deletions src/KubeOps.KubernetesClient/Selectors/LabelSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Different label selectors for querying the Kubernetes API.
/// </summary>
/// <seealso href="https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors">Kubernetes Label Selectors</seealso>
#pragma warning disable S2094
public abstract record LabelSelector : KubernetesSelector;
#pragma warning restore S2094
16 changes: 16 additions & 0 deletions src/KubeOps.KubernetesClient/Selectors/NotEqualsFieldSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Field-selector that checks if a certain field does not equal a specific value.
/// Produces the expression <c>field!=value</c>.
/// </summary>
/// <param name="Field">The field path (e.g. <c>metadata.name</c>).</param>
/// <param name="Value">The excluded value.</param>
public record NotEqualsFieldSelector(string Field, string Value) : FieldSelector
{
protected override string ToExpression() => $"{Field}!={Value}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.LabelSelectors;
namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Label-selector that checks if a certain label does not contain
Expand All @@ -11,7 +11,7 @@ namespace KubeOps.KubernetesClient.LabelSelectors;
/// </summary>
/// <param name="Label">The label that must not equal to one of the values.</param>
/// <param name="Values">The possible values.</param>
public record NotEqualsSelector(string Label, params string[] Values) : LabelSelector
public record NotEqualsLabelSelector(string Label, params string[] Values) : LabelSelector
{
protected override string ToExpression() => $"{Label} notin ({string.Join(",", Values)})";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace KubeOps.KubernetesClient.LabelSelectors;
namespace KubeOps.KubernetesClient.Selectors;

/// <summary>
/// Selector that checks if a certain label does not exist.
/// </summary>
/// <param name="Label">The label that must not exist on the entity/resource.</param>
public record NotExistsSelector(string Label) : LabelSelector
public record NotExistsLabelSelector(string Label) : LabelSelector
{
protected override string ToExpression() => $"!{Label}";
}
Loading
Loading