Skip to content

Commit b2a5008

Browse files
authored
Scoped and Namespaced Clients (#20)
1 parent 7df4959 commit b2a5008

File tree

10 files changed

+228
-46
lines changed

10 files changed

+228
-46
lines changed
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.Extensions.DependencyInjection;
1+
using K8sOperator.NET.Metadata;
2+
using Microsoft.Extensions.DependencyInjection;
23

34
namespace K8sOperator.NET.Builder;
45

@@ -7,6 +8,15 @@ internal class EventWatcherBuilder(IServiceProvider serviceProvider, IController
78
public IEventWatcher Build()
89
{
910
var watcherType = typeof(EventWatcher<>).MakeGenericType(controller.ResourceType);
10-
return (IEventWatcher)ActivatorUtilities.CreateInstance(serviceProvider, watcherType, controller, metadata);
11+
IKubernetesClient client = ActivatorUtilities.CreateInstance<NamespacedKubernetesClient>(serviceProvider);
12+
13+
var clientScope = controller.ResourceType.GetCustomAttributes(false).OfType<IEntityScopeMetadata>().FirstOrDefault();
14+
if (clientScope?.Scope == EntityScope.Cluster)
15+
{
16+
client = ActivatorUtilities.CreateInstance<ClusterKubernetesClient>(serviceProvider);
17+
}
18+
19+
20+
return (IEventWatcher)ActivatorUtilities.CreateInstance(serviceProvider, watcherType, client, controller, metadata);
1121
}
1222
}

src/K8sOperator.NET/Builder/OperatorHostBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.Extensions.Configuration;
44
using Microsoft.Extensions.DependencyInjection;
55
using Microsoft.Extensions.Logging;
6+
using System.Collections.Generic;
67
using System.Data.Common;
78
using System.Reflection;
89

@@ -91,7 +92,10 @@ private void ConfigureMetadata()
9192
var dockerImage = Assembly.GetEntryAssembly()?.GetCustomAttribute<DockerImageAttribute>()
9293
?? DockerImageAttribute.Default;
9394

94-
_metadata.AddRange([operatorName, dockerImage]);
95+
var entityScope = Assembly.GetEntryAssembly()?.GetCustomAttribute<EntityScopeMetadata>()
96+
?? new EntityScopeMetadata(EntityScopeMetadata.Default);
97+
98+
_metadata.AddRange([operatorName, dockerImage, entityScope]);
9599
}
96100
public IConfigurationManager Configuration => _configurationManager;
97101
public IServiceCollection Services => _serviceCollection;

src/K8sOperator.NET/EventWatcher.cs

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using k8s;
2+
using k8s.Autorest;
23
using k8s.Models;
34
using K8sOperator.NET.Extensions;
45
using K8sOperator.NET.Metadata;
@@ -30,7 +31,7 @@ public interface IEventWatcher
3031
Task Start(CancellationToken cancellationToken);
3132
}
3233

33-
internal class EventWatcher<T>(IKubernetes client, Controller<T> controller, List<object> metadata, ILoggerFactory loggerfactory) : IEventWatcher
34+
internal class EventWatcher<T>(IKubernetesClient client, Controller<T> controller, List<object> metadata, ILoggerFactory loggerfactory) : IEventWatcher
3435
where T: CustomResource
3536
{
3637
private KubernetesEntityAttribute Crd => Metadata.OfType<KubernetesEntityAttribute>().First();
@@ -43,7 +44,7 @@ internal class EventWatcher<T>(IKubernetes client, Controller<T> controller, Lis
4344
private CancellationToken _cancellationToken = CancellationToken.None;
4445
private readonly Controller<T> _controller = controller;
4546

46-
public IKubernetes Client { get; } = client;
47+
public IKubernetesClient Client { get; } = client;
4748
public ILogger Logger { get; } = loggerfactory.CreateLogger("watcher");
4849
public IReadOnlyList<object> Metadata { get; } = metadata;
4950
public IController Controller => _controller;
@@ -53,17 +54,7 @@ public async Task Start(CancellationToken cancellationToken)
5354
_cancellationToken = cancellationToken;
5455
_isRunning = true;
5556

56-
var response = Client.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync(
57-
Crd.Group,
58-
Crd.ApiVersion,
59-
Namespace,
60-
Crd.PluralName,
61-
watch: true,
62-
allowWatchBookmarks: true,
63-
labelSelector: LabelSelector,
64-
timeoutSeconds: (int)TimeSpan.FromMinutes(60).TotalSeconds,
65-
cancellationToken: cancellationToken
66-
);
57+
var response = Client.ListAsync<T>(LabelSelector, cancellationToken);
6758

6859
Logger.BeginWatch(Namespace, Crd.PluralName, LabelSelector);
6960

@@ -74,6 +65,7 @@ public async Task Start(CancellationToken cancellationToken)
7465

7566
Logger.EndWatch(Namespace, Crd.PluralName, LabelSelector);
7667
}
68+
7769

7870
private void OnEvent(WatchEventType eventType, T customResource)
7971
{
@@ -206,15 +198,7 @@ private async Task<T> ReplaceAsync(T resource, CancellationToken cancellationTok
206198
Logger.ReplaceResource(resource);
207199

208200
// Replace the resource
209-
var result = await Client.CustomObjects.ReplaceNamespacedCustomObjectAsync<T>(
210-
resource,
211-
Crd.Group,
212-
Crd.ApiVersion,
213-
resource.Metadata.NamespaceProperty,
214-
Crd.PluralName,
215-
resource.Metadata.Name,
216-
cancellationToken: cancellationToken
217-
).ConfigureAwait(false);
201+
var result = await Client.ReplaceAsync(resource, cancellationToken);
218202

219203
return result;
220204
}

src/K8sOperator.NET/Extensions/KubernetesBuilderExtensions.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ public static class KubernetesBuilderExtensions
1515
/// <returns></returns>
1616
public static IServiceCollection AddKubernetes(this IServiceCollection services)
1717
{
18-
KubernetesClientConfiguration config;
18+
services.AddTransient<IKubernetes>(x => {
19+
KubernetesClientConfiguration config;
1920

20-
if (KubernetesClientConfiguration.IsInCluster())
21-
{
22-
config = KubernetesClientConfiguration.InClusterConfig();
23-
}
24-
else
25-
{
26-
config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
27-
}
28-
29-
services.AddSingleton<IKubernetes>(new Kubernetes(config));
21+
if (KubernetesClientConfiguration.IsInCluster())
22+
{
23+
config = KubernetesClientConfiguration.InClusterConfig();
24+
}
25+
else
26+
{
27+
config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
28+
}
29+
30+
return new Kubernetes(config);
31+
});
3032

3133
return services;
3234
}

src/K8sOperator.NET/Extensions/MetaDataExtensions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ public static TBuilder WithGroup<TBuilder>(this TBuilder builder,
6161
public static TBuilder WatchNamespace<TBuilder>(this TBuilder builder, string watchNamespace)
6262
where TBuilder : IControllerConventionBuilder
6363
{
64-
builder.WithSingle(new WatchNamespaceMetadata(watchNamespace));
64+
builder.Finally(x => {
65+
x.Metadata.RemoveAll(x => x.GetType() == typeof(WatchNamespaceMetadata));
66+
x.Metadata.RemoveAll(x => x.GetType() == typeof(EntityScopeMetadata));
67+
x.Metadata.Add(new WatchNamespaceMetadata(watchNamespace));
68+
x.Metadata.Add(new EntityScopeMetadata(EntityScope.Namespaced));
69+
});
6570
return builder;
6671
}
6772

src/K8sOperator.NET/K8sOperator.NET.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3+
<PropertyGroup>
4+
<UserSecretsId>75af4ad7-a38b-4759-92a0-fa12fc4f56dd</UserSecretsId>
5+
</PropertyGroup>
6+
37
<ItemGroup>
48
<PackageReference Include="KubernetesClient" />
59
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using k8s;
2+
using k8s.Autorest;
3+
using k8s.Models;
4+
using K8sOperator.NET.Extensions;
5+
using K8sOperator.NET.Models;
6+
using Microsoft.Extensions.Logging;
7+
using System.Reflection;
8+
9+
namespace K8sOperator.NET;
10+
11+
internal interface IKubernetesClient
12+
{
13+
Task<HttpOperationResponse<object>> ListAsync<T>(string labelselector, CancellationToken cancellationToken)
14+
where T : CustomResource;
15+
Task<T> ReplaceAsync<T>(T resource, CancellationToken cancellationToken)
16+
where T : CustomResource;
17+
18+
19+
}
20+
21+
internal class NamespacedKubernetesClient(IKubernetes client, ILogger<NamespacedKubernetesClient> logger, string ns = "default") : IKubernetesClient
22+
{
23+
public IKubernetes Client { get; } = client;
24+
public ILogger Logger { get; } = logger;
25+
public string Namespace { get; } = ns;
26+
27+
public Task<HttpOperationResponse<object>> ListAsync<T>(string labelselector, CancellationToken cancellationToken) where T : CustomResource
28+
{
29+
var info = typeof(T).GetCustomAttribute<KubernetesEntityAttribute>()!;
30+
31+
var response = Client.CustomObjects.ListNamespacedCustomObjectWithHttpMessagesAsync(
32+
info.Group,
33+
info.ApiVersion,
34+
Namespace,
35+
info.PluralName,
36+
watch: true,
37+
allowWatchBookmarks: true,
38+
labelSelector: labelselector,
39+
timeoutSeconds: (int)TimeSpan.FromMinutes(60).TotalSeconds,
40+
cancellationToken: cancellationToken
41+
);
42+
43+
return response;
44+
}
45+
46+
public async Task<T> ReplaceAsync<T>(T resource, CancellationToken cancellationToken)
47+
where T : CustomResource
48+
{
49+
Logger.ReplaceResource(resource);
50+
51+
var info = resource.GetType().GetCustomAttribute<KubernetesEntityAttribute>()!;
52+
53+
// Replace the resource
54+
var result = await Client.CustomObjects.ReplaceNamespacedCustomObjectAsync<T>(
55+
resource,
56+
info.Group,
57+
info.ApiVersion,
58+
resource.Metadata.NamespaceProperty,
59+
info.PluralName,
60+
resource.Metadata.Name,
61+
cancellationToken: cancellationToken
62+
).ConfigureAwait(false);
63+
64+
return result;
65+
}
66+
}
67+
68+
69+
internal class ClusterKubernetesClient(IKubernetes client, ILogger<NamespacedKubernetesClient> logger) : IKubernetesClient
70+
{
71+
public IKubernetes Client { get; } = client;
72+
public ILogger Logger { get; } = logger;
73+
74+
public async Task<T> ReplaceAsync<T>(T resource, CancellationToken cancellationToken)
75+
where T : CustomResource
76+
{
77+
Logger.ReplaceResource(resource);
78+
79+
var info = resource.GetType().GetCustomAttribute<KubernetesEntityAttribute>()!;
80+
81+
// Replace the resource
82+
var result = await Client.CustomObjects.ReplaceClusterCustomObjectAsync<T>(
83+
resource,
84+
info.Group,
85+
info.ApiVersion,
86+
resource.Metadata.NamespaceProperty,
87+
info.PluralName,
88+
resource.Metadata.Name,
89+
cancellationToken: cancellationToken
90+
).ConfigureAwait(false);
91+
92+
return result;
93+
}
94+
95+
public Task<HttpOperationResponse<object>> ListAsync<T>(string labelselector, CancellationToken cancellationToken)
96+
where T : CustomResource
97+
{
98+
var info = typeof(T).GetCustomAttribute<KubernetesEntityAttribute>()!;
99+
100+
var response = Client.CustomObjects.ListClusterCustomObjectWithHttpMessagesAsync(
101+
info.Group,
102+
info.ApiVersion,
103+
info.PluralName,
104+
watch: true,
105+
allowWatchBookmarks: true,
106+
labelSelector: labelselector,
107+
timeoutSeconds: (int)TimeSpan.FromMinutes(60).TotalSeconds,
108+
cancellationToken: cancellationToken
109+
);
110+
111+
return response;
112+
}
113+
}

src/K8sOperator.NET/Metadata/EntityScopeMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public interface IEntityScopeMetadata
3333
/// Sets the scope of the entity.
3434
/// </summary>
3535
/// <param name="scope"></param>
36-
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
36+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
3737
public class EntityScopeMetadata(EntityScope scope) : Attribute, IEntityScopeMetadata
3838
{
3939
/// <summary>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using k8s;
2+
using K8sOperator.NET.Extensions;
3+
using K8sOperator.NET.Metadata;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
namespace K8sOperator.NET.Tests;
13+
public class EntityScope_Tests
14+
{
15+
[Fact]
16+
public void NamespacedScope_should_use_NamespacedClient()
17+
{
18+
var builder = OperatorHost.CreateOperatorApplicationBuilder();
19+
20+
builder.AddController<TestController>()
21+
.WatchNamespace("test");
22+
23+
builder.Services.RemoveAll<IKubernetes>();
24+
builder.Services.AddTransient(x => Substitute.For<IKubernetes>());
25+
26+
var app = builder.Build();
27+
28+
var watcher = app.DataSource.GetWatchers(app.ServiceProvider).ToList();
29+
30+
watcher.Should().HaveCount(1);
31+
}
32+
33+
[Fact]
34+
public void ClusterScope_should_use_NamespacedClient()
35+
{
36+
var builder = OperatorHost.CreateOperatorApplicationBuilder();
37+
38+
builder.Services.RemoveAll<IKubernetes>();
39+
builder.Services.AddTransient(x => Substitute.For<IKubernetes>());
40+
41+
42+
builder.AddController<Test2Controller>();
43+
44+
var app = builder.Build();
45+
46+
var watcher = app.DataSource.GetWatchers(app.ServiceProvider).ToList();
47+
48+
watcher.Should().HaveCount(1);
49+
}
50+
51+
private class TestController : Controller<TestResource> { }
52+
53+
private class Test2Controller : Controller<ClusterResource> { }
54+
55+
[EntityScopeMetadata(EntityScope.Cluster)]
56+
public class ClusterResource : TestResource
57+
{
58+
59+
}
60+
}

0 commit comments

Comments
 (0)