Skip to content

Commit b5e1a8e

Browse files
author
Christoph Bühler
committed
feat(testing): add possibility to mock and test a controller via mocked event queue.
This closes #8.
1 parent f8c5f76 commit b5e1a8e

File tree

13 files changed

+474
-54
lines changed

13 files changed

+474
-54
lines changed

DotnetOperatorSdk.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{9AF95FE4
1414
EndProject
1515
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.TestOperator", "tests\KubeOps.TestOperator\KubeOps.TestOperator.csproj", "{751BDC14-D75F-4DDE-9C45-2432041FBCAC}"
1616
EndProject
17+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KubeOps.TestOperator.Test", "tests\KubeOps.TestOperator.Test\KubeOps.TestOperator.Test.csproj", "{B374D7E4-E9BA-47F8-B1A4-440DECD376E4}"
18+
EndProject
1719
Global
1820
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1921
Debug|Any CPU = Debug|Any CPU
@@ -34,11 +36,16 @@ Global
3436
{751BDC14-D75F-4DDE-9C45-2432041FBCAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
3537
{751BDC14-D75F-4DDE-9C45-2432041FBCAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
3638
{751BDC14-D75F-4DDE-9C45-2432041FBCAC}.Release|Any CPU.Build.0 = Release|Any CPU
39+
{B374D7E4-E9BA-47F8-B1A4-440DECD376E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40+
{B374D7E4-E9BA-47F8-B1A4-440DECD376E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
41+
{B374D7E4-E9BA-47F8-B1A4-440DECD376E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
42+
{B374D7E4-E9BA-47F8-B1A4-440DECD376E4}.Release|Any CPU.Build.0 = Release|Any CPU
3743
EndGlobalSection
3844
GlobalSection(NestedProjects) = preSolution
3945
{D7AB6CB9-94B6-4FEB-B7D8-D8AA793BD2A4} = {95F3A6DD-B421-441D-B263-1B34A1465FF5}
4046
{A33D30D0-AC1B-48F8-8A5A-36E569981793} = {50E9B964-68F7-4B9F-BEA8-165CE45BC5C6}
4147
{D47717CB-A02E-4B12-BAA8-1D7F8BAE9BBD} = {9AF95FE4-DA1F-4BB0-B60E-23FCFA6AAAA2}
4248
{751BDC14-D75F-4DDE-9C45-2432041FBCAC} = {50E9B964-68F7-4B9F-BEA8-165CE45BC5C6}
49+
{B374D7E4-E9BA-47F8-B1A4-440DECD376E4} = {50E9B964-68F7-4B9F-BEA8-165CE45BC5C6}
4350
EndGlobalSection
4451
EndGlobal

src/KubeOps/KubeOps.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,4 @@
4747
</AssemblyAttribute>
4848
</ItemGroup>
4949

50-
<ItemGroup>
51-
<Folder Include="Testing" />
52-
</ItemGroup>
53-
5450
</Project>

src/KubeOps/Operator/KubernetesOperator.cs

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using KubeOps.Operator.Queue;
1313
using KubeOps.Operator.Serialization;
1414
using KubeOps.Operator.Watcher;
15+
using KubeOps.Testing;
1516
using McMaster.Extensions.CommandLineUtils;
1617
using Microsoft.Extensions.DependencyInjection;
1718
using Microsoft.Extensions.Hosting;
@@ -23,38 +24,94 @@
2324

2425
namespace KubeOps.Operator
2526
{
26-
public sealed class KubernetesOperator
27+
public class KubernetesOperator
2728
{
2829
internal const string NoStructuredLogs = "--no-structured-logs";
2930
private const string DefaultOperatorName = "KubernetesOperator";
30-
private readonly OperatorSettings _operatorSettings;
3131

32-
private readonly IHostBuilder _builder = Host
32+
protected readonly OperatorSettings OperatorSettings;
33+
34+
protected readonly IList<Action<IServiceCollection>> ServiceConfigurations =
35+
new List<Action<IServiceCollection>>();
36+
37+
protected readonly IHostBuilder Builder = Host
3338
.CreateDefaultBuilder()
3439
.UseConsoleLifetime();
3540

36-
public KubernetesOperator(string? operatorName = null)
41+
public KubernetesOperator()
42+
: this((Assembly.GetEntryAssembly()?.GetName().Name ?? DefaultOperatorName).ToLowerInvariant())
43+
{
44+
}
45+
46+
public KubernetesOperator(string operatorName)
47+
: this(
48+
new OperatorSettings
49+
{
50+
Name = operatorName,
51+
})
3752
{
38-
operatorName ??= (operatorName ?? Assembly.GetEntryAssembly()?.GetName().Name ?? DefaultOperatorName)
39-
.ToLowerInvariant();
40-
_operatorSettings = new OperatorSettings
41-
{
42-
Name = operatorName,
43-
};
4453
}
4554

4655
public KubernetesOperator(OperatorSettings settings)
4756
{
48-
_operatorSettings = settings;
57+
OperatorSettings = settings;
4958
}
5059

51-
public Task<int> Run(string[] args)
60+
public KubernetesTestOperator ToKubernetesTestOperator()
5261
{
53-
ConfigureRequiredServices();
62+
var op = new KubernetesTestOperator
63+
{
64+
OperatorSettings = { Name = OperatorSettings.Name }
65+
};
66+
67+
foreach (var config in ServiceConfigurations)
68+
{
69+
op.ConfigureServices(config);
70+
}
71+
72+
return op;
73+
}
74+
75+
public Task<int> Run() => Run(new string[0]);
76+
77+
public virtual Task<int> Run(string[] args)
78+
{
79+
ConfigureOperatorServices();
5480

5581
var app = new CommandLineApplication<RunOperator>();
5682

57-
_builder.ConfigureLogging(
83+
ConfigureOperatorLogging(args);
84+
85+
var host = Builder.Build();
86+
87+
app
88+
.Conventions
89+
.UseDefaultConventions()
90+
.UseConstructorInjection(host.Services);
91+
92+
DependencyInjector.Services = host.Services;
93+
JsonConvert.DefaultSettings = () => host.Services.GetRequiredService<JsonSerializerSettings>();
94+
95+
return app.ExecuteAsync(args);
96+
}
97+
98+
public KubernetesOperator ConfigureServices(Action<IServiceCollection> configuration)
99+
{
100+
ServiceConfigurations.Add(configuration);
101+
return this;
102+
}
103+
104+
protected virtual void ConfigureOperatorServices()
105+
{
106+
ConfigureRequiredServices();
107+
foreach (var config in ServiceConfigurations)
108+
{
109+
Builder.ConfigureServices(config);
110+
}
111+
}
112+
113+
protected virtual void ConfigureOperatorLogging(IEnumerable<string> args) =>
114+
Builder.ConfigureLogging(
58115
(hostContext, logging) =>
59116
{
60117
logging.ClearProviders();
@@ -63,11 +120,12 @@ public Task<int> Run(string[] args)
63120
{
64121
if (args.Contains(NoStructuredLogs))
65122
{
66-
logging.AddConsole(options =>
67-
{
68-
options.TimestampFormat = @"[dd.MM.yyyy - HH:mm:ss] ";
69-
options.DisableColors = true;
70-
});
123+
logging.AddConsole(
124+
options =>
125+
{
126+
options.TimestampFormat = @"[dd.MM.yyyy - HH:mm:ss] ";
127+
options.DisableColors = true;
128+
});
71129
}
72130
else
73131
{
@@ -80,37 +138,18 @@ public Task<int> Run(string[] args)
80138
}
81139
});
82140

83-
var host = _builder.Build();
84-
85-
app
86-
.Conventions
87-
.UseDefaultConventions()
88-
.UseConstructorInjection(host.Services);
89-
90-
DependencyInjector.Services = host.Services;
91-
JsonConvert.DefaultSettings = () => host.Services.GetRequiredService<JsonSerializerSettings>();
92-
93-
return app.ExecuteAsync(args);
94-
}
95-
96-
public KubernetesOperator ConfigureServices(Action<IServiceCollection> configuration)
97-
{
98-
_builder.ConfigureServices(configuration);
99-
return this;
100-
}
101-
102141
private void ConfigureRequiredServices() =>
103-
_builder.ConfigureServices(
142+
Builder.ConfigureServices(
104143
services =>
105144
{
106-
services.AddSingleton(_operatorSettings);
145+
services.AddSingleton(OperatorSettings);
107146

108147
services.AddTransient(
109148
_ => new JsonSerializerSettings
110149
{
111150
ContractResolver = new NamingConvention(),
112151
Converters = new List<JsonConverter>
113-
{new StringEnumConverter {NamingStrategy = new CamelCaseNamingStrategy()}},
152+
{ new StringEnumConverter { NamingStrategy = new CamelCaseNamingStrategy() } },
114153
});
115154
services.AddTransient(
116155
_ => new SerializerBuilder()
@@ -131,13 +170,13 @@ private void ConfigureRequiredServices() =>
131170
{
132171
ContractResolver = new NamingConvention(),
133172
Converters = new List<JsonConverter>
134-
{new StringEnumConverter {NamingStrategy = new CamelCaseNamingStrategy()}}
173+
{ new StringEnumConverter { NamingStrategy = new CamelCaseNamingStrategy() } }
135174
},
136175
DeserializationSettings =
137176
{
138177
ContractResolver = new NamingConvention(),
139178
Converters = new List<JsonConverter>
140-
{new StringEnumConverter {NamingStrategy = new CamelCaseNamingStrategy()}}
179+
{ new StringEnumConverter { NamingStrategy = new CamelCaseNamingStrategy() } }
141180
}
142181
};
143182
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using k8s;
5+
using k8s.Models;
6+
using KubeOps.Operator;
7+
using KubeOps.Operator.DependencyInjection;
8+
using KubeOps.Operator.Queue;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.DependencyInjection.Extensions;
11+
using Microsoft.Extensions.Hosting;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace KubeOps.Testing
15+
{
16+
public class KubernetesTestOperator : KubernetesOperator, IDisposable
17+
{
18+
public IServiceProvider Services => DependencyInjector.Services;
19+
20+
public MockResourceEventQueue<TEntity> GetMockedEventQueue<TEntity>()
21+
where TEntity : IKubernetesObject<V1ObjectMeta>
22+
=> Services.GetRequiredService<MockResourceQueueCollection>().Get<TEntity>();
23+
24+
public void Dispose()
25+
{
26+
Services.GetService<IHost>()?.StopAsync();
27+
}
28+
29+
public override Task<int> Run(string[] args)
30+
{
31+
base.Run(args).ConfigureAwait(false);
32+
return Task.FromResult(0);
33+
}
34+
35+
protected override void ConfigureOperatorServices()
36+
{
37+
ConfigureServices(
38+
services =>
39+
{
40+
services.RemoveAll(typeof(IResourceEventQueue<>));
41+
services.AddTransient(typeof(IResourceEventQueue<>), typeof(MockResourceEventQueue<>));
42+
services.AddSingleton<MockResourceQueueCollection>();
43+
});
44+
base.ConfigureOperatorServices();
45+
}
46+
47+
protected override void ConfigureOperatorLogging(IEnumerable<string> _) =>
48+
Builder.ConfigureLogging(
49+
(__, logging) => { logging.ClearProviders(); });
50+
}
51+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using k8s;
5+
using k8s.Models;
6+
using KubeOps.Operator.Queue;
7+
8+
namespace KubeOps.Testing
9+
{
10+
public class MockResourceEventQueue<TEntity> : IResourceEventQueue<TEntity>
11+
where TEntity : IKubernetesObject<V1ObjectMeta>
12+
{
13+
private readonly MockResourceQueueCollection _collection;
14+
15+
public event EventHandler<(ResourceEventType type, TEntity resource)>? ResourceEvent;
16+
17+
public MockResourceEventQueue(MockResourceQueueCollection collection)
18+
{
19+
_collection = collection;
20+
}
21+
22+
public IList<TEntity> Enqueued { get; } = new List<TEntity>();
23+
24+
public IList<(ResourceEventType, TEntity)> ErrorEnqueued { get; } = new List<(ResourceEventType, TEntity)>();
25+
26+
public void Dispose()
27+
{
28+
}
29+
30+
public Task Start()
31+
{
32+
_collection.Register(this);
33+
return Task.CompletedTask;
34+
}
35+
36+
public Task Stop()
37+
{
38+
_collection.Unregister(this);
39+
return Task.CompletedTask;
40+
}
41+
42+
public void Created(TEntity entity) => Fire(ResourceEventType.Created, entity);
43+
44+
public void Updated(TEntity entity) => Fire(ResourceEventType.Updated, entity);
45+
46+
public void Deleted(TEntity entity) => Fire(ResourceEventType.Deleted, entity);
47+
48+
public void NotModified(TEntity entity) => Fire(ResourceEventType.NotModified, entity);
49+
50+
public void StatusUpdated(TEntity entity) => Fire(ResourceEventType.StatusUpdated, entity);
51+
52+
public void Finalizing(TEntity entity) => Fire(ResourceEventType.Finalizing, entity);
53+
54+
public Task Enqueue(TEntity resource, TimeSpan? enqueueDelay = null)
55+
{
56+
Enqueued.Add(resource);
57+
return Task.CompletedTask;
58+
}
59+
60+
public void EnqueueErrored(ResourceEventType type, TEntity resource) => ErrorEnqueued.Add((type, resource));
61+
62+
public void ClearError(TEntity resource)
63+
{
64+
}
65+
66+
private void Fire(ResourceEventType type, TEntity entity) => ResourceEvent?.Invoke(this, (type, entity));
67+
}
68+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using k8s;
4+
using k8s.Models;
5+
using KubeOps.Operator.Watcher;
6+
7+
namespace KubeOps.Testing
8+
{
9+
public class MockResourceQueueCollection
10+
{
11+
private readonly IDictionary<Type, object> _queues =
12+
new Dictionary<Type, object>();
13+
14+
public void Register<TEntity>(MockResourceEventQueue<TEntity> watcher)
15+
where TEntity : IKubernetesObject<V1ObjectMeta>
16+
{
17+
_queues.Add(typeof(TEntity), watcher);
18+
}
19+
20+
public void Unregister<TEntity>(MockResourceEventQueue<TEntity> _)
21+
where TEntity : IKubernetesObject<V1ObjectMeta>
22+
{
23+
_queues.Remove(typeof(TEntity));
24+
}
25+
26+
public MockResourceEventQueue<TEntity> Get<TEntity>()
27+
where TEntity : IKubernetesObject<V1ObjectMeta>
28+
=> (MockResourceEventQueue<TEntity>) _queues[typeof(TEntity)];
29+
}
30+
}

0 commit comments

Comments
 (0)