Skip to content

Commit a63b511

Browse files
committed
refactor(validators): Use scoped validators and register by type.
BREAKING CHANGE: Validators do not need to be registered via `AddValidationWebhook` method. All validators are infered/searched by type when they implement the `IValidationWebhook<>` interface. When the validation webhook is called, a scope is created.
1 parent 0817dd0 commit a63b511

21 files changed

+255
-267
lines changed

src/KubeOps/Operator/ApplicationBuilderExtensions.cs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Reflection;
55
using KubeOps.Operator.Builder;
6+
using KubeOps.Operator.Services;
67
using KubeOps.Operator.Webhooks;
78
using Microsoft.AspNetCore.Builder;
89
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
@@ -45,26 +46,17 @@ public static void UseKubernetesOperator(
4546
.GetRequiredService<ILoggerFactory>()
4647
.CreateLogger("ApplicationStartup");
4748

48-
foreach (var validator in app.ApplicationServices
49-
.GetService<IEnumerable<IValidationWebhook>>() ??
50-
new List<IValidationWebhook>())
49+
using var scope = app.ApplicationServices.CreateScope();
50+
foreach (var (validatorType, resourceType) in scope
51+
.ServiceProvider
52+
.GetRequiredService<ResourceLocator>()
53+
.ValidatorTypes)
5154
{
52-
var hookType = validator
53-
.GetType()
54-
.GetInterfaces()
55-
.FirstOrDefault(
56-
t => t.IsGenericType &&
57-
typeof(IValidationWebhook<>).IsAssignableFrom(t.GetGenericTypeDefinition()));
58-
if (hookType == null)
59-
{
60-
throw new Exception(
61-
$@"Validator ""{validator.GetType().Name}"" is not of IValidationWebhook<TEntity> type.");
62-
}
63-
64-
var registerMethod = hookType
55+
var validator = scope.ServiceProvider.GetRequiredService(validatorType);
56+
var registerMethod = typeof(IValidationWebhook<>)
57+
.MakeGenericType(resourceType)
6558
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
6659
.First(m => m.Name == "Register");
67-
6860
registerMethod.Invoke(validator, new object[] { endpoints });
6961
logger.LogInformation(
7062
@"Registered validation webhook ""{namespace}.{name}"".",

src/KubeOps/Operator/Builder/IOperatorBuilder.cs

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,14 @@ IOperatorBuilder AddLivenessCheck<TLivenessCheck>(string? name = default)
5353
where TLivenessCheck : class, IHealthCheck;
5454

5555
/// <summary>
56-
/// Adds an assembly to the resource search path. This allows the given Assembly to searched
56+
/// <para>
57+
/// Adds an assembly to the resource search path. This allows the given Assembly to be searched
5758
/// for resources when generating CRDs or RBAC definitions.
59+
/// </para>
60+
/// <para>Also webhooks / controllers and finalizers will be searched in this assembly.</para>
5861
/// </summary>
5962
/// <param name="assembly">The assembly to add.</param>
6063
/// <returns>The builder for chaining.</returns>
6164
IOperatorBuilder AddResourceAssembly(Assembly assembly);
62-
63-
/// <summary>
64-
/// <para>
65-
/// Add an <see cref="IValidationWebhook{TEntity}"/> to the system.
66-
/// The validator is registered as POST request on the system.
67-
/// </para>
68-
/// <para>
69-
/// Note that the operator must use HTTPS (in some form) to use validators.
70-
/// For local development, this could be done with ngrok (https://ngrok.com/).
71-
/// </para>
72-
/// <para>
73-
/// The operator generator (if enabled during build) will generate the CA certificate
74-
/// and the operator will generate the server certificate during pod startup.
75-
/// </para>
76-
/// </summary>
77-
/// <typeparam name="TWebhook">
78-
/// The type of the finalizer.
79-
/// This must be a <see cref="IValidationWebhook{TEntity}"/> instead of "only" a
80-
/// <see cref="IValidationWebhook"/>.
81-
/// </typeparam>
82-
/// <returns>The builder for chaining.</returns>
83-
IOperatorBuilder AddValidationWebhook<TWebhook>()
84-
where TWebhook : class, IValidationWebhook;
8565
}
8666
}

src/KubeOps/Operator/Builder/OperatorBuilder.cs

Lines changed: 16 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using System.Reflection;
54
using DotnetKubernetesClient;
65
using k8s;
@@ -14,7 +13,6 @@
1413
using KubeOps.Operator.Leadership;
1514
using KubeOps.Operator.Serialization;
1615
using KubeOps.Operator.Services;
17-
using KubeOps.Operator.Webhooks;
1816
using Microsoft.Extensions.DependencyInjection;
1917
using Microsoft.Extensions.DependencyInjection.Extensions;
2018
using Microsoft.Extensions.Diagnostics.HealthChecks;
@@ -31,18 +29,16 @@ internal class OperatorBuilder : IOperatorBuilder
3129
internal const string LivenessTag = "liveness";
3230
internal const string ReadinessTag = "readiness";
3331

34-
internal static readonly Assembly[] Assemblies =
35-
{
36-
Assembly.GetEntryAssembly() ?? throw new Exception("No Entry Assembly found."),
37-
Assembly.GetExecutingAssembly(),
38-
};
39-
40-
private readonly IResourceTypeService _resourceTypeService;
32+
private readonly ResourceLocator _resourceLocator = new(
33+
new[]
34+
{
35+
Assembly.GetEntryAssembly() ?? throw new Exception("No Entry Assembly found."),
36+
Assembly.GetExecutingAssembly(),
37+
});
4138

4239
public OperatorBuilder(IServiceCollection services)
4340
{
4441
Services = services;
45-
_resourceTypeService = new ResourceTypeService(Assemblies);
4642
}
4743

4844
public IServiceCollection Services { get; }
@@ -85,43 +81,10 @@ public IOperatorBuilder AddLivenessCheck<TLivenessCheck>(string? name = default)
8581

8682
public IOperatorBuilder AddResourceAssembly(Assembly assembly)
8783
{
88-
_resourceTypeService.AddAssembly(assembly);
89-
84+
_resourceLocator.Add(assembly);
9085
return this;
9186
}
9287

93-
public IOperatorBuilder AddValidationWebhook<TWebhook>()
94-
where TWebhook : class, IValidationWebhook
95-
{
96-
Services.AddTransient(typeof(IValidationWebhook), typeof(TWebhook));
97-
Services.AddTransient(typeof(TWebhook));
98-
99-
return this;
100-
}
101-
102-
internal static IEnumerable<(Type ControllerType, Type EntityType)> GetControllers() => Assemblies
103-
.SelectMany(a => a.GetTypes())
104-
.Where(
105-
t => t.IsClass &&
106-
!t.IsAbstract &&
107-
t.GetInterfaces()
108-
.Any(
109-
i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IResourceController<>)))
110-
.Select(
111-
t => (t,
112-
t.GetInterfaces()
113-
.First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IResourceController<>))
114-
.GenericTypeArguments[0]));
115-
116-
internal static IEnumerable<Type> GetFinalizers() => Assemblies
117-
.SelectMany(a => a.GetTypes())
118-
.Where(
119-
t => t.IsClass &&
120-
!t.IsAbstract &&
121-
t.GetInterfaces()
122-
.Any(
123-
i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IResourceFinalizer<>)));
124-
12588
internal IOperatorBuilder AddOperatorBase(OperatorSettings settings)
12689
{
12790
Services.AddSingleton(settings);
@@ -156,7 +119,7 @@ internal IOperatorBuilder AddOperatorBase(OperatorSettings settings)
156119
Services.AddTransient<IKubernetesClient, KubernetesClient>();
157120
Services.AddTransient<IEventManager, EventManager>();
158121

159-
Services.AddSingleton(_resourceTypeService);
122+
Services.AddSingleton(_resourceLocator);
160123

161124
Services.AddTransient(typeof(ResourceCache<>));
162125
Services.AddTransient(typeof(ResourceWatcher<>));
@@ -183,18 +146,24 @@ internal IOperatorBuilder AddOperatorBase(OperatorSettings settings)
183146
// and all found controller types.
184147
Services.AddHostedService<ResourceControllerManager>();
185148
Services.TryAddSingleton(sp => sp);
186-
foreach (var (controllerType, _) in GetControllers())
149+
foreach (var (controllerType, _) in _resourceLocator.ControllerTypes)
187150
{
188151
Services.TryAddScoped(controllerType);
189152
}
190153

191154
// Register all found finalizer for the finalize manager
192155
Services.AddTransient(typeof(IFinalizerManager<>), typeof(FinalizerManager<>));
193-
foreach (var finalizerType in GetFinalizers())
156+
foreach (var finalizerType in _resourceLocator.FinalizerTypes)
194157
{
195158
Services.TryAddScoped(finalizerType);
196159
}
197160

161+
// Register all found validation webhooks
162+
foreach (var (validatorType, _) in _resourceLocator.ValidatorTypes)
163+
{
164+
Services.TryAddScoped(validatorType);
165+
}
166+
198167
return this;
199168
}
200169
}

src/KubeOps/Operator/Commands/Generators/CrdGenerator.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.IO;
43
using System.Linq;
54
using System.Reflection;
6-
using System.Text;
75
using System.Threading.Tasks;
86
using k8s.Models;
97
using k8s.Versioning;
@@ -22,20 +20,20 @@ internal class CrdGenerator : GeneratorBase
2220
{
2321
private readonly EntitySerializer _serializer;
2422

25-
private readonly IResourceTypeService _resourceTypeService;
23+
private readonly ResourceLocator _resourceLocator;
2624

27-
public CrdGenerator(EntitySerializer serializer, IResourceTypeService resourceTypeService)
25+
public CrdGenerator(EntitySerializer serializer, ResourceLocator resourceLocator)
2826
{
2927
_serializer = serializer;
30-
_resourceTypeService = resourceTypeService;
28+
_resourceLocator = resourceLocator;
3129
}
3230

3331
[Option("--use-old-crds", Description = "Defines that the old crd definitions (V1Beta1) should be used.")]
3432
public bool UseOldCrds { get; set; }
3533

36-
public static IEnumerable<V1CustomResourceDefinition> GenerateCrds(IResourceTypeService resourceTypeService)
34+
public static IEnumerable<V1CustomResourceDefinition> GenerateCrds(ResourceLocator resourceTypeService)
3735
{
38-
var resourceTypes = resourceTypeService.GetResourceTypesByAttribute<KubernetesEntityAttribute>();
36+
var resourceTypes = resourceTypeService.GetTypesWithAttribute<KubernetesEntityAttribute>();
3937

4038
return resourceTypes
4139
.Where(type => !type.GetCustomAttributes<IgnoreEntityAttribute>().Any())
@@ -75,7 +73,7 @@ public static IEnumerable<V1CustomResourceDefinition> GenerateCrds(IResourceType
7573

7674
public async Task<int> OnExecuteAsync(CommandLineApplication app)
7775
{
78-
var crds = GenerateCrds(_resourceTypeService).ToList();
76+
var crds = GenerateCrds(_resourceLocator).ToList();
7977

8078
var fileWriter = new FileWriter(app.Out);
8179
foreach (var crd in crds)

src/KubeOps/Operator/Commands/Generators/OperatorGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using KubeOps.Operator.Commands.CommandHelpers;
77
using KubeOps.Operator.Entities.Kustomize;
88
using KubeOps.Operator.Serialization;
9-
using KubeOps.Operator.Webhooks;
9+
using KubeOps.Operator.Services;
1010
using McMaster.Extensions.CommandLineUtils;
1111

1212
namespace KubeOps.Operator.Commands.Generators
@@ -21,11 +21,11 @@ internal class OperatorGenerator : GeneratorBase
2121
public OperatorGenerator(
2222
EntitySerializer serializer,
2323
OperatorSettings settings,
24-
IEnumerable<IValidationWebhook> validators)
24+
ResourceLocator resourceLocator)
2525
{
2626
_serializer = serializer;
2727
_settings = settings;
28-
_hasWebhooks = validators.Any();
28+
_hasWebhooks = resourceLocator.ValidatorTypes.Any();
2929
}
3030

3131
public async Task<int> OnExecuteAsync(CommandLineApplication app)

src/KubeOps/Operator/Commands/Generators/RbacGenerator.cs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
using System.Collections.Generic;
2-
using System.IO;
32
using System.Linq;
4-
using System.Text;
53
using System.Threading.Tasks;
64
using k8s.Models;
75
using KubeOps.Operator.Commands.CommandHelpers;
86
using KubeOps.Operator.Entities.Kustomize;
97
using KubeOps.Operator.Rbac;
108
using KubeOps.Operator.Serialization;
119
using KubeOps.Operator.Services;
12-
using KubeOps.Operator.Webhooks;
1310
using McMaster.Extensions.CommandLineUtils;
1411

1512
namespace KubeOps.Operator.Commands.Generators
@@ -18,25 +15,24 @@ namespace KubeOps.Operator.Commands.Generators
1815
internal class RbacGenerator : GeneratorBase
1916
{
2017
private readonly EntitySerializer _serializer;
21-
private readonly IResourceTypeService _resourceTypeService;
18+
private readonly ResourceLocator _resourceLocator;
2219
private readonly bool _hasWebhooks;
2320

2421
public RbacGenerator(
2522
EntitySerializer serializer,
26-
IResourceTypeService resourceTypeService,
27-
IEnumerable<IValidationWebhook> validators)
23+
ResourceLocator resourceLocator)
2824
{
2925
_serializer = serializer;
30-
_resourceTypeService = resourceTypeService;
31-
_hasWebhooks = validators.Any();
26+
_resourceLocator = resourceLocator;
27+
_hasWebhooks = resourceLocator.ValidatorTypes.Any();
3228
}
3329

34-
public V1ClusterRole GenerateManagerRbac(IResourceTypeService resourceTypeService)
30+
public V1ClusterRole GenerateManagerRbac(ResourceLocator resourceTypeService)
3531
{
36-
var entityRbacPolicyRules = resourceTypeService.GetResourceAttributes<EntityRbacAttribute>()
32+
var entityRbacPolicyRules = resourceTypeService.GetAttributes<EntityRbacAttribute>()
3733
.SelectMany(attribute => attribute.CreateRbacPolicies());
3834

39-
var genericRbacPolicyRules = resourceTypeService.GetResourceAttributes<GenericRbacAttribute>()
35+
var genericRbacPolicyRules = resourceTypeService.GetAttributes<GenericRbacAttribute>()
4036
.Select(attribute => attribute.CreateRbacPolicy());
4137

4238
var rules = entityRbacPolicyRules.Concat(genericRbacPolicyRules).ToList();
@@ -66,7 +62,7 @@ public async Task<int> OnExecuteAsync(CommandLineApplication app)
6662
var fileWriter = new FileWriter(app.Out);
6763
fileWriter.Add(
6864
$"operator-role.{Format.ToString().ToLower()}",
69-
_serializer.Serialize(GenerateManagerRbac(_resourceTypeService), Format));
65+
_serializer.Serialize(GenerateManagerRbac(_resourceLocator), Format));
7066
fileWriter.Add(
7167
$"operator-role-binding.{Format.ToString().ToLower()}",
7268
_serializer.Serialize(

src/KubeOps/Operator/Commands/Management/Install.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@ internal class Install
2020
{
2121
private readonly IKubernetesClient _client;
2222

23-
private readonly IResourceTypeService _resourceTypeService;
23+
private readonly ResourceLocator _resourceLocator;
2424

25-
public Install(IKubernetesClient client, IResourceTypeService resourceTypeService)
25+
public Install(IKubernetesClient client, ResourceLocator resourceLocator)
2626
{
2727
_client = client;
28-
_resourceTypeService = resourceTypeService;
28+
_resourceLocator = resourceLocator;
2929
}
3030

3131
public async Task<int> OnExecuteAsync(CommandLineApplication app)
3232
{
3333
var error = false;
34-
var crds = CrdGenerator.GenerateCrds(_resourceTypeService).ToList();
34+
var crds = CrdGenerator.GenerateCrds(_resourceLocator).ToList();
3535
await app.Out.WriteLineAsync($"Found {crds.Count} CRD's.");
3636
await app.Out.WriteLineAsync($@"Starting install into cluster with url ""{_client.ApiClient.BaseUri}"".");
3737

src/KubeOps/Operator/Commands/Management/Uninstall.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ internal class Uninstall
2020
{
2121
private readonly IKubernetesClient _client;
2222

23-
private readonly IResourceTypeService _resourceTypeService;
23+
private readonly ResourceLocator _resourceLocator;
2424

25-
public Uninstall(IKubernetesClient client, IResourceTypeService resourceTypeService)
25+
public Uninstall(IKubernetesClient client, ResourceLocator resourceLocator)
2626
{
2727
_client = client;
28-
_resourceTypeService = resourceTypeService;
28+
_resourceLocator = resourceLocator;
2929
}
3030

3131
[Option(Description = "Do not ask the user if the uninstall should proceed.")]
@@ -34,7 +34,7 @@ public Uninstall(IKubernetesClient client, IResourceTypeService resourceTypeServ
3434
public async Task<int> OnExecuteAsync(CommandLineApplication app)
3535
{
3636
var error = false;
37-
var crds = CrdGenerator.GenerateCrds(_resourceTypeService).ToList();
37+
var crds = CrdGenerator.GenerateCrds(_resourceLocator).ToList();
3838
await app.Out.WriteLineAsync($"Found {crds.Count} CRD's.");
3939

4040
if (!Force && !Prompt.GetYesNo("Should the uninstall proceed?", false, ConsoleColor.Red))

0 commit comments

Comments
 (0)