Skip to content

Commit 3537772

Browse files
authored
feat(events): Add event support for resources and controllers. (#132)
This closes #5. The provided IEventManager helps with publishing singleton events (fire and forget) and event series. The event-name is deterministic, so if the same event is published multiple times, an event series is created (when used with Publish(string,string...)).
1 parent 89e5258 commit 3537772

File tree

11 files changed

+539
-6
lines changed

11 files changed

+539
-6
lines changed

docs/docs/events.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Events
2+
3+
Kubernetes knows "Events" which can be sort of attached to a resource
4+
(i.e. a Kubernetes object).
5+
6+
To create and use events, inject the @"KubeOps.Operator.Events.IEventManager"
7+
into your controller. It is registered as a transient resource in the DI
8+
container.
9+
10+
## IEventManager
11+
12+
### Publish events
13+
14+
The event manager allows you to either publish an event that you created
15+
by yourself, or helps you publish events with predefined data.
16+
17+
If you want to use the helper:
18+
```c#
19+
// fetch from DI, or inject into your controller.
20+
IEventManager manager = services.GetRequiredService<IEventManager>;
21+
22+
// Publish the event.
23+
// This creates an event and publishes it.
24+
// If the event was previously published, it is fetched
25+
// and the "count" number is increased. This essentially
26+
// creates an event-series.
27+
await manager.Publish(resource, "reason", "my fancy message");
28+
```
29+
30+
If you want full control over the event:
31+
```c#
32+
// fetch from DI, or inject into your controller.
33+
IEventManager manager = services.GetRequiredService<IEventManager>;
34+
35+
var @event = new Corev1Event
36+
{
37+
// ... fill out all fields.
38+
}
39+
40+
// Publish the event.
41+
// This essentially calls IKubernetesClient.Save.
42+
await manager.Publish(@event);
43+
```
44+
45+
### Use publisher delegates
46+
47+
If you don't want to call the
48+
@"KubeOps.Operator.Events.IEventManager.Publish(k8s.IKubernetesObject{k8s.Models.V1ObjectMeta},System.String,System.String,KubeOps.Operator.Events.EventType)"
49+
all the time with the same arguments, you can create delegates.
50+
51+
There exist two different delegates:
52+
- @"KubeOps.Operator.Events.IEventManager.StaticPublisher": Predefined event
53+
on a predefined resource.
54+
- @"KubeOps.Operator.Events.IEventManager.Publisher": Predefined event
55+
on a variable resource.
56+
57+
Both are created with their specific overload:
58+
- @"KubeOps.Operator.Events.IEventManager.CreatePublisher(k8s.IKubernetesObject{k8s.Models.V1ObjectMeta},System.String,System.String,KubeOps.Operator.Events.EventType)"
59+
- @"KubeOps.Operator.Events.IEventManager.CreatePublisher(System.String,System.String,KubeOps.Operator.Events.EventType)"
60+
61+
To use the static publisher:
62+
```c#
63+
var publisher = manager.CreatePublisher(resource, "reason", "message");
64+
await publisher();
65+
66+
// and later on:
67+
await publisher(); // again without specifying reason / message and so on.
68+
```
69+
70+
To use the dynamic publisher:
71+
```c#
72+
var publisher = manager.CreatePublisher("reason", "message");
73+
await publisher(resource);
74+
75+
// and later on:
76+
await publisher(resource); // again without specifying reason / message and so on.
77+
```
78+
79+
The dynamic publisher can be used to predefine the event for your resources.
80+
81+
As an example in a controller:
82+
```c#
83+
public class TestController : ResourceControllerBase<V1TestEntity>
84+
{
85+
private readonly IEventManager.Publisher _publisher;
86+
87+
public TestController(IEventManager eventManager, IResourceServices<V1TestEntity> services)
88+
: base(services)
89+
{
90+
_publisher = eventManager.CreatePublisher("reason", "my fancy message");
91+
}
92+
93+
protected override async Task<TimeSpan?> Created(V1TestEntity resource)
94+
{
95+
// Here, the event is published with predefined strings
96+
// but for a "variable" resource.
97+
await _publisher(resource);
98+
return await base.Created(resource);
99+
}
100+
}
101+
```

docs/docs/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
href: entities.md
99
- name: Controller
1010
href: controller.md
11+
- name: Events
12+
href: events.md
1113
- name: Finalizer
1214
href: finalizer.md
1315
- name: Utilities

docs/docs/utilities.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ to see which metrics are available.
3535

3636
Of course you can also have a look at the used metrics classes to see the
3737
implementation: [Metrics Implementations](https://github.com/buehler/dotnet-operator-sdk/tree/master/src/KubeOps/Operator/DevOps).
38+
39+
# Entity / Resource utils
40+
41+
There are several method extensions that help with day to day resource
42+
handling. Head over to their documentation to see that they do:
43+
44+
- @"KubeOps.Operator.Entities.Extensions.KubernetesObjectExtensions.MakeObjectReference(k8s.IKubernetesObject{k8s.Models.V1ObjectMeta})"
45+
- @"KubeOps.Operator.Entities.Extensions.KubernetesObjectExtensions.MakeOwnerReference(k8s.IKubernetesObject{k8s.Models.V1ObjectMeta})"
46+
- @"KubeOps.Operator.Entities.Extensions.KubernetesObjectExtensions.WithOwnerReference``1(``0,k8s.IKubernetesObject{k8s.Models.V1ObjectMeta})"

src/KubeOps/KubeOps.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<PackageReference Include="Namotion.Reflection" Version="1.0.15" />
3333
<PackageReference Include="prometheus-net.AspNetCore" Version="4.1.1" />
3434
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="4.1.1" />
35+
<PackageReference Include="SimpleBase" Version="3.0.2" />
3536
<PackageReference Include="YamlDotNet" Version="9.1.4" />
3637
</ItemGroup>
3738

src/KubeOps/Operator/Builder/OperatorBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using KubeOps.Operator.Caching;
66
using KubeOps.Operator.Controller;
77
using KubeOps.Operator.DevOps;
8+
using KubeOps.Operator.Events;
89
using KubeOps.Operator.Finalizer;
910
using KubeOps.Operator.Leadership;
1011
using KubeOps.Operator.Queue;
@@ -136,6 +137,7 @@ internal IOperatorBuilder AddOperatorBase(OperatorSettings settings)
136137
Services.AddTransient<EntitySerializer>();
137138

138139
Services.AddTransient<IKubernetesClient, KubernetesClient>();
140+
Services.AddTransient<IEventManager, EventManager>();
139141

140142
Services.AddSingleton(typeof(IResourceCache<>), typeof(ResourceCache<>));
141143
Services.AddTransient(typeof(IResourceWatcher<>), typeof(ResourceWatcher<>));
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Security.Cryptography;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using DotnetKubernetesClient;
7+
using k8s;
8+
using k8s.Models;
9+
using KubeOps.Operator.Entities.Extensions;
10+
using Microsoft.Extensions.Logging;
11+
using SimpleBase;
12+
13+
namespace KubeOps.Operator.Events
14+
{
15+
internal class EventManager : IEventManager
16+
{
17+
private readonly IKubernetesClient _client;
18+
private readonly OperatorSettings _settings;
19+
private readonly ILogger<EventManager> _logger;
20+
21+
public EventManager(IKubernetesClient client, OperatorSettings settings, ILogger<EventManager> logger)
22+
{
23+
_client = client;
24+
_settings = settings;
25+
_logger = logger;
26+
}
27+
28+
public async Task Publish(
29+
IKubernetesObject<V1ObjectMeta> resource,
30+
string reason,
31+
string message,
32+
EventType type = EventType.Normal)
33+
{
34+
_logger.LogTrace(
35+
"Encoding event name with: {resourceName}.{resourceNamespace}.{reason}.{message}.{type}.",
36+
resource.Name(),
37+
resource.Namespace(),
38+
reason,
39+
message,
40+
type);
41+
var eventName =
42+
Base32.Rfc4648.Encode(
43+
SHA512.HashData(
44+
Encoding.UTF8.GetBytes($"{resource.Name()}.{resource.Namespace()}.{reason}.{message}.{type}")));
45+
_logger.LogTrace(@"Search or create event with name ""{name}"".", eventName);
46+
var @event = await _client.Get<Corev1Event>(eventName, resource.Namespace()) ??
47+
new Corev1Event
48+
{
49+
Kind = Corev1Event.KubeKind,
50+
ApiVersion = $"{Corev1Event.KubeGroup}/{Corev1Event.KubeApiVersion}",
51+
Metadata = new V1ObjectMeta
52+
{
53+
Name = eventName,
54+
NamespaceProperty = resource.Namespace(),
55+
Annotations = new Dictionary<string, string>
56+
{
57+
{ "nameHash", "sha512" },
58+
{ "nameEncoding", "Base32 / RFC 4648" },
59+
},
60+
},
61+
Type = type.ToString(),
62+
Reason = reason,
63+
Message = message,
64+
ReportingComponent = _settings.Name,
65+
ReportingInstance = Environment.MachineName,
66+
Source = new V1EventSource { Component = _settings.Name },
67+
InvolvedObject = resource.MakeObjectReference(),
68+
FirstTimestamp = DateTime.UtcNow,
69+
LastTimestamp = DateTime.UtcNow,
70+
Count = 0,
71+
};
72+
73+
@event.Count++;
74+
@event.LastTimestamp = DateTime.UtcNow;
75+
_logger.LogTrace(
76+
"Save event with new count {count} and last timestamp {timestamp}",
77+
@event.Count,
78+
@event.LastTimestamp);
79+
80+
await _client.Save(@event);
81+
_logger.LogDebug(@"Created or updated event with name ""{name}"".", eventName);
82+
}
83+
84+
public Task Publish(Corev1Event @event)
85+
=> _client.Save(@event);
86+
87+
public IEventManager.Publisher CreatePublisher(string reason, string message, EventType type = EventType.Normal)
88+
=> resource => Publish(resource, reason, message, type);
89+
90+
public IEventManager.StaticPublisher CreatePublisher(
91+
IKubernetesObject<V1ObjectMeta> resource,
92+
string reason,
93+
string message,
94+
EventType type = EventType.Normal)
95+
=> () => Publish(resource, reason, message, type);
96+
}
97+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using k8s.Models;
2+
3+
namespace KubeOps.Operator.Events
4+
{
5+
/// <summary>
6+
/// The type of a <see cref="Corev1Event"/>.
7+
/// The event type will be stringified and used as <see cref="Corev1Event.Type"/>.
8+
/// </summary>
9+
public enum EventType
10+
{
11+
/// <summary>
12+
/// A normal event, informative value.
13+
/// </summary>
14+
Normal,
15+
16+
/// <summary>
17+
/// A warning, something might went wrong.
18+
/// </summary>
19+
Warning,
20+
}
21+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Threading.Tasks;
2+
using k8s;
3+
using k8s.Models;
4+
5+
namespace KubeOps.Operator.Events
6+
{
7+
/// <summary>
8+
/// Event manager for <see cref="Corev1Event"/> objects.
9+
/// Contains various utility methods for emitting events on objects.
10+
/// </summary>
11+
public interface IEventManager
12+
{
13+
/// <summary>
14+
/// Delegate that publishes a predefined event for a statically given resource.
15+
/// This delegate should be created with
16+
/// <see cref="IEventManager.CreatePublisher(IKubernetesObject{k8s.Models.V1ObjectMeta},string,string,KubeOps.Operator.Events.EventType)"/>.
17+
/// When called, the publisher creates or updates the event defined by the params.
18+
/// </summary>
19+
/// <returns>A task that completes when the event is published.</returns>
20+
public delegate Task StaticPublisher();
21+
22+
/// <summary>
23+
/// Delegate that publishes a predefined event for a resource that is passed.
24+
/// This delegate should be created with
25+
/// <see cref="IEventManager.CreatePublisher(string,string,KubeOps.Operator.Events.EventType)"/>.
26+
/// When called with a resource, the publisher creates or updates the event defined by the params.
27+
/// </summary>
28+
/// <param name="resource">The resource on which the event should be published.</param>
29+
/// <returns>A task that completes when the event is published.</returns>
30+
public delegate Task Publisher(IKubernetesObject<V1ObjectMeta> resource);
31+
32+
/// <summary>
33+
/// Publish an event in relation to a given resource.
34+
/// The event is created or updated if it exists.
35+
/// </summary>
36+
/// <param name="resource">The resource that is involved with the event.</param>
37+
/// <param name="reason">The reason string. This should be a machine readable reason string.</param>
38+
/// <param name="message">A human readable string for the event.</param>
39+
/// <param name="type">The type of the event.</param>
40+
/// <returns>A task that finishes when the event is created or updated.</returns>
41+
Task Publish(
42+
IKubernetesObject<V1ObjectMeta> resource,
43+
string reason,
44+
string message,
45+
EventType type = EventType.Normal);
46+
47+
/// <summary>
48+
/// Create or update an event.
49+
/// </summary>
50+
/// <param name="event">The full event object that should be created or updated.</param>
51+
/// <returns>A task that finishes when the event is created or updated.</returns>
52+
Task Publish(Corev1Event @event);
53+
54+
/// <summary>
55+
/// Create a <see cref="Publisher"/> for a predefined event.
56+
/// The <see cref="Publisher"/> is then called with a resource (<see cref="IKubernetesObject{V1ObjectMeta}"/>).
57+
/// The predefined event is published with this resource as the involved object.
58+
/// </summary>
59+
/// <param name="reason">The reason string. This should be a machine readable reason string.</param>
60+
/// <param name="message">A human readable string for the event.</param>
61+
/// <param name="type">The type of the event.</param>
62+
/// <returns>A <see cref="Publisher"/> delegate that can be called to create or update events.</returns>
63+
Publisher CreatePublisher(
64+
string reason,
65+
string message,
66+
EventType type = EventType.Normal);
67+
68+
/// <summary>
69+
/// Create a <see cref="StaticPublisher"/> for a predefined event.
70+
/// The <see cref="StaticPublisher"/> is then called without any parameters.
71+
/// The predefined event is published with the initially given resource as the involved object.
72+
/// </summary>
73+
/// <param name="resource">The resource that is involved with the event.</param>
74+
/// <param name="reason">The reason string. This should be a machine readable reason string.</param>
75+
/// <param name="message">A human readable string for the event.</param>
76+
/// <param name="type">The type of the event.</param>
77+
/// <returns>A <see cref="StaticPublisher"/> delegate that can be called to create or update events.</returns>
78+
StaticPublisher CreatePublisher(
79+
IKubernetesObject<V1ObjectMeta> resource,
80+
string reason,
81+
string message,
82+
EventType type = EventType.Normal);
83+
}
84+
}

0 commit comments

Comments
 (0)