Skip to content

Commit 367689a

Browse files
authored
refactor: Dependency Injection Registration and OperatorBuilder
This allows one controller to support more than one entity type and/or one entity-type to be managed by more than one controller. It is not recommended to implement the latter case, as the watchers will pass the same object version to all controllers and there will be race conditions as all controllers are called simultaneously. All the `...Type` classes and their generic variants are removed in favor of adding registrations in `IComponentRegistrar` Additionally, any special logic for instantiating these registrations should be encapsulated in separate classes such as with `IControllerInstanceBuilder` in order to avoid violating SRP as much as possible (introducing that much reflection can really distract from what a class is really meant to handle). BREAKING CHANGE: `EntityRbacAttribute` and `GenericRbacAttribute` are now only found when added to Kubernetes "relevant" classes. Instead of scanning all assemblies for those attributes, only controllers, finalizers and webhooks may be decorated and considered when generating the RBAC rules in the generator. To migrate, add any of the attributes above to a controller, finalizer or webhook instead of anywhere else in the code.
1 parent bc13961 commit 367689a

File tree

55 files changed

+1518
-696
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1518
-696
lines changed

config/Common.targets

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
1313
<_Parameter1>$(MSBuildProjectName).Test</_Parameter1>
1414
</AssemblyAttribute>
15+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
16+
<_Parameter1>$(MSBuildProjectName).TestOperator.Test</_Parameter1>
17+
</AssemblyAttribute>
1518
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
1619
<!--So that internals are visible to the Moq library-->
1720
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>

docs/docs/advanced.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Advanced Topics
2+
3+
## Assembly Scanning
4+
5+
By default, KubeOps scans the assembly containing the main entrypoint for
6+
controller, finalizer, webhook and entity types, and automatically registers
7+
all types that implement the correct interfaces for usage.
8+
9+
If some of the above are stored in a different assembly, KubeOps must be
10+
specifically instructed to scan that assembly <xref:KubeOps.Operator.Builder.IOperatorBuilder.AddResourceAssembly(System.Reflection.Assembly)> or else those types won't be loaded.
11+
12+
```csharp
13+
public class Startup
14+
{
15+
public void ConfigureServices(IServiceCollection services)
16+
{
17+
services.AddKubernetesOperator()
18+
.AddResourceAssembly(typeof(CustomEntityController).Assembly)
19+
}
20+
21+
public void Configure(IApplicationBuilder app)
22+
{
23+
app.UseKubernetesOperator();
24+
}
25+
}
26+
```
27+
28+
## Manual Registration
29+
30+
If desired, the default behavior of assembly scanning can be disabled so
31+
specific components can be registered manually. (Using both methods in parallel
32+
is supported, such as if you want to load all components from one assembly and
33+
only some from another.)
34+
35+
See <xref:KubeOps.Operator.Builder.IOperatorBuilder> for details on the methods
36+
utilized in this registration pattern.
37+
38+
```csharp
39+
public class Startup
40+
{
41+
public void ConfigureServices(IServiceCollection services)
42+
{
43+
services.AddKubernetesOperator(settings =>
44+
{
45+
settings.EnableAssemblyScanning = false;
46+
})
47+
.AddEntity<V1DemoEntityClone>()
48+
.AddController<DemoController, V1DemoEntityClone>()
49+
.AddController<DemoControllerClone>()
50+
.AddFinalizer<DemoFinalizer>()
51+
.AddValidationWebhook<DemoValidator>()
52+
.AddMutationWebhook<DemoMutator>();
53+
}
54+
55+
public void Configure(IApplicationBuilder app)
56+
{
57+
app.UseKubernetesOperator();
58+
}
59+
}
60+
```

docs/docs/finalizer.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,24 @@ public class TestController : IResourceController<V1TestEntity>
6565
}
6666
}
6767
```
68+
69+
Alternatively, the <xref:KubeOps.Operator.Finalizer.IFinalizerManager`1.RegisterAllFinalizersAsync(`0)>
70+
method can be used to attach all finalizers known to the operator for that entity type.
71+
72+
```csharp
73+
public class TestController : IResourceController<V1TestEntity>
74+
{
75+
private readonly IFinalizerManager<V1TestEntity> _manager;
76+
77+
public TestController(IFinalizerManager<V1TestEntity> manager)
78+
{
79+
_manager = manager;
80+
}
81+
82+
public async Task<ResourceControllerResult> CreatedAsync(V1TestEntity resource)
83+
{
84+
await _manager.RegisterAllFinalizersAsync(resource);
85+
return null;
86+
}
87+
}
88+
```

docs/docs/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
href: utilities.md
1919
- name: CLI Commands
2020
href: commands.md
21+
- name: Advanced Topics
22+
href: advanced.md
2123
- name: MS Build extensions
2224
href: ms_build.md
2325
- name: Testing

src/KubeOps.Testing/KubernetesOperatorFactory.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using DotnetKubernetesClient;
55
using k8s;
66
using k8s.Models;
7+
using KubeOps.Operator.Builder;
78
using KubeOps.Operator.Controller;
89
using KubeOps.Operator.Kubernetes;
910
using KubeOps.Operator.Leadership;
@@ -57,17 +58,15 @@ public void Run()
5758
public Task EnqueueEvent<TEntity>(ResourceEventType type, TEntity resource)
5859
where TEntity : class, IKubernetesObject<V1ObjectMeta>
5960
{
60-
var controller = Services.GetRequiredService<ManagedResourceController<TEntity>>() as
61-
MockManagedResourceController<TEntity>;
61+
var controller = GetMockController<TEntity>();
6262

6363
return controller?.EnqueueEvent(type, resource) ?? Task.CompletedTask;
6464
}
6565

6666
public Task EnqueueFinalization<TEntity>(TEntity resource)
6767
where TEntity : class, IKubernetesObject<V1ObjectMeta>
6868
{
69-
var controller = Services.GetRequiredService<ManagedResourceController<TEntity>>() as
70-
MockManagedResourceController<TEntity>;
69+
var controller = GetMockController<TEntity>();
7170

7271
return controller?.EnqueueFinalization(resource) ?? Task.CompletedTask;
7372
}
@@ -106,10 +105,22 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
106105
services.RemoveAll(typeof(IKubernetesClient));
107106
services.AddSingleton<IKubernetesClient, MockKubernetesClient>();
108107

109-
services.RemoveAll(typeof(ManagedResourceController<>));
110-
services.AddSingleton(typeof(ManagedResourceController<>), typeof(MockManagedResourceController<>));
108+
services.RemoveAll<Func<IComponentRegistrar.ControllerRegistration, IManagedResourceController>>();
109+
services.AddSingleton(
110+
s => (Func<IComponentRegistrar.ControllerRegistration, IManagedResourceController>)(r =>
111+
(IManagedResourceController)ActivatorUtilities.CreateInstance(
112+
s,
113+
typeof(MockManagedResourceController<>).MakeGenericType(r.EntityType),
114+
r)));
111115
});
112116
builder.ConfigureLogging(logging => logging.ClearProviders());
113117
}
118+
119+
private MockManagedResourceController<TEntity>? GetMockController<TEntity>()
120+
where TEntity : class, IKubernetesObject<V1ObjectMeta> =>
121+
Services.GetRequiredService<IControllerInstanceBuilder>()
122+
.BuildControllers<TEntity>()
123+
.OfType<MockManagedResourceController<TEntity>>()
124+
.FirstOrDefault();
114125
}
115126
}

src/KubeOps.Testing/MockManagedResourceController{TResource}.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
using KubeOps.Operator.Caching;
88
using KubeOps.Operator.Controller;
99
using KubeOps.Operator.DevOps;
10-
using KubeOps.Operator.Finalizer;
1110
using KubeOps.Operator.Kubernetes;
1211
using Microsoft.Extensions.Logging;
12+
using static KubeOps.Operator.Builder.IComponentRegistrar;
1313

1414
namespace KubeOps.Testing
1515
{
@@ -24,8 +24,8 @@ public MockManagedResourceController(
2424
IServiceProvider services,
2525
ResourceControllerMetrics<TEntity> metrics,
2626
OperatorSettings settings,
27-
IFinalizerManager<TEntity> finalizerManager)
28-
: base(logger, client, watcher, cache, services, metrics, settings, finalizerManager)
27+
ControllerRegistration controllerRegistration)
28+
: base(logger, client, watcher, cache, services, metrics, settings, controllerRegistration)
2929
{
3030
}
3131

src/KubeOps/Operator/ApplicationBuilderExtensions.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
using System.Linq;
1+
using System;
2+
using System.Linq;
23
using System.Reflection;
34
using KubeOps.Operator.Builder;
4-
using KubeOps.Operator.Services;
55
using KubeOps.Operator.Webhooks;
66
using Microsoft.AspNetCore.Builder;
77
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
@@ -45,32 +45,39 @@ public static void UseKubernetesOperator(
4545
.CreateLogger("ApplicationStartup");
4646

4747
using var scope = app.ApplicationServices.CreateScope();
48-
var locator = scope.ServiceProvider.GetRequiredService<ResourceLocator>();
48+
var componentRegistrar = scope.ServiceProvider.GetRequiredService<IComponentRegistrar>();
49+
var webhookMetadataBuilder = scope.ServiceProvider.GetRequiredService<IWebhookMetadataBuilder>();
4950

50-
foreach (var (validatorType, resourceType) in locator.ValidatorTypes)
51+
foreach (var wh in componentRegistrar.ValidatorRegistrations)
5152
{
53+
(Type validatorType, Type entityType) = wh;
54+
5255
var validator = scope.ServiceProvider.GetRequiredService(validatorType);
5356
var registerMethod = typeof(IAdmissionWebhook<,>)
54-
.MakeGenericType(resourceType, typeof(ValidationResult))
57+
.MakeGenericType(entityType, typeof(ValidationResult))
5558
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
5659
.First(m => m.Name == "Register");
5760
registerMethod.Invoke(validator, new object[] { endpoints });
58-
var (name, endpoint) = Webhooks.Webhooks.Metadata<ValidationResult>(validator, resourceType);
61+
var (name, endpoint) =
62+
webhookMetadataBuilder.GetMetadata<ValidationResult>(validator, entityType);
5963
logger.LogInformation(
6064
@"Registered validation webhook ""{name}"" under ""{endpoint}"".",
6165
name,
6266
endpoint);
6367
}
6468

65-
foreach (var (mutatorType, resourceType) in locator.MutatorTypes)
69+
foreach (var wh in componentRegistrar.MutatorRegistrations)
6670
{
71+
(Type mutatorType, Type entityType) = wh;
72+
6773
var mutator = scope.ServiceProvider.GetRequiredService(mutatorType);
6874
var registerMethod = typeof(IAdmissionWebhook<,>)
69-
.MakeGenericType(resourceType, typeof(MutationResult))
75+
.MakeGenericType(entityType, typeof(MutationResult))
7076
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
7177
.First(m => m.Name == "Register");
7278
registerMethod.Invoke(mutator, new object[] { endpoints });
73-
var (name, endpoint) = Webhooks.Webhooks.Metadata<MutationResult>(mutator, resourceType);
79+
var (name, endpoint) =
80+
webhookMetadataBuilder.GetMetadata<MutationResult>(mutator, entityType);
7481
logger.LogInformation(
7582
@"Registered mutation webhook ""{name}"" under ""{endpoint}"".",
7683
name,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using k8s;
6+
using k8s.Models;
7+
using KubeOps.Operator.Controller;
8+
using KubeOps.Operator.Finalizer;
9+
using KubeOps.Operator.Webhooks;
10+
11+
namespace KubeOps.Operator.Builder
12+
{
13+
internal class AssemblyScanner : IAssemblyScanner
14+
{
15+
private readonly IOperatorBuilder _operatorBuilder;
16+
private readonly List<(Type Type, MethodInfo RegistrationMethod)> _registrationDefinitions;
17+
18+
public AssemblyScanner(IOperatorBuilder operatorBuilder)
19+
{
20+
_operatorBuilder = operatorBuilder;
21+
22+
var operatorBuilderMethods = typeof(OperatorBuilderExtensions).GetMethods(
23+
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
24+
25+
_registrationDefinitions =
26+
new (Type Type, string MethodName)[]
27+
{
28+
new() { Type = typeof(IKubernetesObject<V1ObjectMeta>), MethodName = nameof(OperatorBuilderExtensions.AddEntity) },
29+
new() { Type = typeof(IResourceController<>), MethodName = nameof(OperatorBuilderExtensions.AddController) },
30+
new() { Type = typeof(IResourceFinalizer<>), MethodName = nameof(OperatorBuilderExtensions.AddFinalizer) },
31+
new() { Type = typeof(IValidationWebhook<>), MethodName = nameof(OperatorBuilderExtensions.AddValidationWebhook) },
32+
new() { Type = typeof(IMutationWebhook<>), MethodName = nameof(OperatorBuilderExtensions.AddMutationWebhook) },
33+
}
34+
.Select<(Type Type, string MethodName), (Type Type, MethodInfo RegistrationMethod)>(
35+
t => new()
36+
{
37+
Type = t.Type,
38+
RegistrationMethod = operatorBuilderMethods.Single(
39+
m => m.Name == t.MethodName && m.GetGenericArguments().Length == 1),
40+
})
41+
.ToList();
42+
}
43+
44+
public IAssemblyScanner AddAssembly(Assembly assembly)
45+
{
46+
var types = assembly.GetTypes()
47+
.Where(t => (t.Attributes & TypeAttributes.Abstract) == 0);
48+
49+
var registrationMethods = _registrationDefinitions.Join(
50+
types,
51+
_ => 1,
52+
_ => 1,
53+
(registrationDefinition, type) =>
54+
new { RegistrationDefinition = registrationDefinition, ComponentType = type })
55+
.Where(
56+
t => t.ComponentType.GetInterfaces()
57+
.Any(
58+
i => (i.IsConstructedGenericType && i.GetGenericTypeDefinition().IsEquivalentTo(t.RegistrationDefinition.Type)) ||
59+
(t.RegistrationDefinition.Type.IsConstructedGenericType && i.IsEquivalentTo(t.RegistrationDefinition.Type))))
60+
.Select(
61+
t => t.RegistrationDefinition.RegistrationMethod.MakeGenericMethod(t.ComponentType));
62+
63+
foreach (var method in registrationMethods)
64+
{
65+
method.Invoke(null, new object[] { _operatorBuilder });
66+
}
67+
68+
return this;
69+
}
70+
}
71+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Collections.Generic;
2+
using System.Collections.Immutable;
3+
using k8s;
4+
using k8s.Models;
5+
using KubeOps.Operator.Controller;
6+
using KubeOps.Operator.Finalizer;
7+
using KubeOps.Operator.Webhooks;
8+
using static KubeOps.Operator.Builder.IComponentRegistrar;
9+
10+
namespace KubeOps.Operator.Builder
11+
{
12+
internal class ComponentRegistrar : IComponentRegistrar
13+
{
14+
private readonly HashSet<EntityRegistration> _entityRegistrations = new();
15+
private readonly HashSet<ControllerRegistration> _controllerRegistrations = new();
16+
private readonly HashSet<FinalizerRegistration> _finalizerRegistrations = new();
17+
private readonly HashSet<ValidatorRegistration> _validatorRegistrations = new();
18+
private readonly HashSet<MutatorRegistration> _mutatorRegistrations = new();
19+
20+
public ImmutableHashSet<EntityRegistration> EntityRegistrations => _entityRegistrations.ToImmutableHashSet();
21+
22+
public ImmutableHashSet<ControllerRegistration> ControllerRegistrations => _controllerRegistrations.ToImmutableHashSet();
23+
24+
public ImmutableHashSet<FinalizerRegistration> FinalizerRegistrations => _finalizerRegistrations.ToImmutableHashSet();
25+
26+
public ImmutableHashSet<ValidatorRegistration> ValidatorRegistrations => _validatorRegistrations.ToImmutableHashSet();
27+
28+
public ImmutableHashSet<MutatorRegistration> MutatorRegistrations => _mutatorRegistrations.ToImmutableHashSet();
29+
30+
public IComponentRegistrar RegisterEntity<TEntity>()
31+
where TEntity : IKubernetesObject<V1ObjectMeta>
32+
{
33+
_entityRegistrations.Add(new EntityRegistration(typeof(TEntity)));
34+
35+
return this;
36+
}
37+
38+
public IComponentRegistrar RegisterController<TController, TEntity>()
39+
where TController : class, IResourceController<TEntity>
40+
where TEntity : IKubernetesObject<V1ObjectMeta>
41+
{
42+
_controllerRegistrations.Add(new ControllerRegistration(typeof(TController), typeof(TEntity)));
43+
44+
return RegisterEntity<TEntity>();
45+
}
46+
47+
public IComponentRegistrar RegisterFinalizer<TFinalizer, TEntity>()
48+
where TFinalizer : class, IResourceFinalizer<TEntity>
49+
where TEntity : IKubernetesObject<V1ObjectMeta>
50+
{
51+
_finalizerRegistrations.Add(new FinalizerRegistration(typeof(TFinalizer), typeof(TEntity)));
52+
53+
return this;
54+
}
55+
56+
public IComponentRegistrar RegisterValidator<TValidator, TEntity>()
57+
where TValidator : class, IValidationWebhook<TEntity>
58+
where TEntity : IKubernetesObject<V1ObjectMeta>
59+
{
60+
_validatorRegistrations.Add(new ValidatorRegistration(typeof(TValidator), typeof(TEntity)));
61+
62+
return this;
63+
}
64+
65+
public IComponentRegistrar RegisterMutator<TMutator, TEntity>()
66+
where TMutator : class, IMutationWebhook<TEntity>
67+
where TEntity : IKubernetesObject<V1ObjectMeta>
68+
{
69+
_mutatorRegistrations.Add(new MutatorRegistration(typeof(TMutator), typeof(TEntity)));
70+
71+
return RegisterEntity<TEntity>();
72+
}
73+
}
74+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace KubeOps.Operator.Builder
5+
{
6+
internal class DisabledAssemblyScanner : IAssemblyScanner
7+
{
8+
public IAssemblyScanner AddAssembly(Assembly assembly)
9+
{
10+
throw new InvalidOperationException("Assembly scanning is disabled by current operator configuration.");
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)