Skip to content

Commit c6ba4a6

Browse files
authored
feat(operator): add CRD installer utility for development purposes (#908)
Signed-off-by: Christoph Bühler <[email protected]>
1 parent e86ea83 commit c6ba4a6

File tree

10 files changed

+256
-88
lines changed

10 files changed

+256
-88
lines changed

docs/docs/operator/building-blocks/entities.mdx

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -154,43 +154,3 @@ public class V1DemoEntity : CustomKubernetesEntity<V1DemoEntity.V1DemoEntitySpec
154154
}
155155
}
156156
```
157-
158-
## Serialization Helper: KubernetesJsonSerializer
159-
160-
When working with custom entities, you may need to serialize or deserialize them to and from JSON, especially when interacting with the Kubernetes API or for testing purposes. KubeOps provides a helper class, `KubernetesJsonSerializer`, to make this process straightforward and consistent with Kubernetes conventions.
161-
162-
### Example Usage
163-
164-
#### Get the Kubernetes specific JsonSerializerOptions
165-
166-
```csharp
167-
var options = KubernetesJsonSerializer.SerializerOptions;
168-
```
169-
170-
#### Serialize an Entity
171-
172-
```csharp
173-
var entity = new V1DemoEntity { /* ... initialize ... */ };
174-
string json = KubernetesJsonSerializer.Serialize(entity);
175-
```
176-
177-
#### Deserialize an Entity
178-
179-
```csharp
180-
string json = /* JSON string from Kubernetes */;
181-
var entity = KubernetesJsonSerializer.Deserialize<V1DemoEntity>(json);
182-
```
183-
184-
#### With Custom JsonSerializerOptions
185-
186-
```csharp
187-
var options = new JsonSerializerOptions { WriteIndented = true };
188-
string json = KubernetesJsonSerializer.Serialize(entity, options);
189-
```
190-
191-
### API Overview
192-
193-
- `Serialize(object value, JsonSerializerOptions? options = null)`: Serializes an object to a JSON string.
194-
- `Deserialize<T>(...)`: Deserializes JSON (from string, stream, `JsonDocument`, `JsonElement`, or `JsonNode`) to a strongly-typed object.
195-
196-
This helper ensures your custom entities are always serialized and deserialized in a way that's compatible with Kubernetes expectations.

docs/docs/operator/utilities.mdx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
title: Utilities
3+
description: Utilities for your Operator and Development
4+
sidebar_position: 9
5+
---
6+
7+
# Development and Operator Utilities
8+
9+
## Serialization Helper
10+
11+
When working with custom entities, you may need to serialize or deserialize them to and from JSON, especially when interacting with the Kubernetes API or for testing purposes. `KubeOps` provides a helper class, `KubernetesJsonSerializer`, to make this process straightforward and consistent with Kubernetes conventions.
12+
13+
### Example Usage
14+
15+
#### Get the Kubernetes specific JsonSerializerOptions
16+
17+
```csharp
18+
var options = KubernetesJsonSerializer.SerializerOptions;
19+
```
20+
21+
#### Serialize an Entity
22+
23+
```csharp
24+
var entity = new V1DemoEntity { /* ... initialize ... */ };
25+
string json = KubernetesJsonSerializer.Serialize(entity);
26+
```
27+
28+
#### Deserialize an Entity
29+
30+
```csharp
31+
string json = /* JSON string from Kubernetes */;
32+
var entity = KubernetesJsonSerializer.Deserialize<V1DemoEntity>(json);
33+
```
34+
35+
#### With Custom JsonSerializerOptions
36+
37+
```csharp
38+
var options = new JsonSerializerOptions { WriteIndented = true };
39+
string json = KubernetesJsonSerializer.Serialize(entity, options);
40+
```
41+
42+
### API Overview
43+
44+
- `Serialize(object value, JsonSerializerOptions? options = null)`: Serializes an object to a JSON string.
45+
- `Deserialize<T>(...)`: Deserializes JSON (from string, stream, `JsonDocument`, `JsonElement`, or `JsonNode`) to a strongly-typed object.
46+
47+
This helper ensures your custom entities are always serialized and deserialized in a way that's compatible with Kubernetes expectations.
48+
49+
## CRD Installer Utility
50+
51+
:::warning Destructive Utility
52+
The CRD Installer is a powerful utility intended **only for development environments**. Depending on its settings, it can overwrite or delete existing CRDs, which may lead to data loss or cluster instability. **Never use this in production!**
53+
:::
54+
55+
When developing operators, you may want to quickly install or update CustomResourceDefinitions (CRDs) in your cluster. The `CrdInstaller` service automates this process, making it easier to iterate on CRD changes during development.
56+
57+
### How to Add the CRD Installer
58+
59+
To enable the CRD installer, add the following to your operator's `Program.cs`:
60+
61+
```csharp
62+
builder.Services
63+
.AddKubernetesOperator()
64+
#if DEBUG
65+
.AddCrdInstaller(c =>
66+
{
67+
c.OverwriteExisting = true;
68+
c.DeleteOnShutdown = true;
69+
})
70+
#endif
71+
.RegisterComponents();
72+
```
73+
74+
- `OverwriteExisting`: If `true`, existing CRDs with the same name will be **overwritten**. This is useful for development but can be destructive if used in production, as it may cause data loss.
75+
- `DeleteOnShutdown`: If `true`, all CRDs installed by the operator will be **deleted** when the operator shuts down. This is extremely destructive and should only be used in disposable development environments.

examples/ConversionWebhookOperator/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
// Configure Kestrel to listen on IPv4, use port 443, and use the server certificate
1919
builder.WebHost.ConfigureKestrel(serverOptions =>
2020
{
21-
serverOptions.Listen(IPAddress.Any, port, async listenOptions =>
21+
serverOptions.Listen(IPAddress.Any, port, listenOptions =>
2222
{
2323
listenOptions.UseHttps(cert);
2424
});

examples/Operator/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99

1010
builder.Services
1111
.AddKubernetesOperator()
12+
#if DEBUG
13+
.AddCrdInstaller(c =>
14+
{
15+
c.OverwriteExisting = true;
16+
c.DeleteOnShutdown = true;
17+
})
18+
#endif
1219
.RegisterComponents();
1320

1421
using var host = builder.Build();

examples/WebhookOperator/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
builder.WebHost.ConfigureKestrel(serverOptions =>
1919
{
20-
serverOptions.Listen(IPAddress.Any, port, async listenOptions =>
20+
serverOptions.Listen(IPAddress.Any, port, listenOptions =>
2121
{
2222
listenOptions.UseHttps(cert);
2323
});

src/KubeOps.Abstractions/Builder/IOperatorBuilder.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using k8s.Models;
33

44
using KubeOps.Abstractions.Controller;
5+
using KubeOps.Abstractions.Crds;
56
using KubeOps.Abstractions.Entities;
67
using KubeOps.Abstractions.Finalizer;
78

@@ -60,4 +61,18 @@ IOperatorBuilder AddController<TImplementation, TEntity, TLabelSelector>()
6061
IOperatorBuilder AddFinalizer<TImplementation, TEntity>(string identifier)
6162
where TImplementation : class, IEntityFinalizer<TEntity>
6263
where TEntity : IKubernetesObject<V1ObjectMeta>;
64+
65+
/// <summary>
66+
/// Adds a hosted service to the operator that installs the CRDs for the operator
67+
/// on startup. Note that this will only install the CRDs in the current assembly.
68+
/// Also, the operator may be destructive if current installed CRDs are overwritten!
69+
/// This is intended for development purposes only.
70+
/// </summary>
71+
/// <param name="configure">
72+
/// Configuration action for the <see cref="CrdInstallerSettings"/>.
73+
/// Determines the behavior of the CRD installer, such as whether existing CRDs
74+
/// should be overwritten or deleted on shutdown.
75+
/// </param>
76+
/// <returns>The builder for chaining.</returns>
77+
IOperatorBuilder AddCrdInstaller(Action<CrdInstallerSettings>? configure = null);
6378
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace KubeOps.Abstractions.Crds;
2+
3+
/// <summary>
4+
/// Settings for the CRD installer.
5+
/// </summary>
6+
public sealed class CrdInstallerSettings
7+
{
8+
/// <summary>
9+
/// Determines whether existing CRDs should be overwritten.
10+
/// This is useful for development purposes and should be used with caution.
11+
/// It is a destructive operation that may lead to data loss.
12+
/// </summary>
13+
public bool OverwriteExisting { get; set; } = false;
14+
15+
/// <summary>
16+
/// Determines whether the installed CRDs should be deleted when the operator shuts down.
17+
/// This is a very destructive operation and should only be used in development environments.
18+
/// </summary>
19+
public bool DeleteOnShutdown { get; set; } = false;
20+
}

src/KubeOps.Operator/Builder/OperatorBuilder.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
using KubeOps.Abstractions.Builder;
77
using KubeOps.Abstractions.Controller;
8+
using KubeOps.Abstractions.Crds;
89
using KubeOps.Abstractions.Entities;
910
using KubeOps.Abstractions.Events;
1011
using KubeOps.Abstractions.Finalizer;
1112
using KubeOps.Abstractions.Queue;
1213
using KubeOps.KubernetesClient;
14+
using KubeOps.Operator.Crds;
1315
using KubeOps.Operator.Events;
1416
using KubeOps.Operator.Finalizer;
1517
using KubeOps.Operator.LeaderElection;
@@ -95,6 +97,15 @@ public IOperatorBuilder AddFinalizer<TImplementation, TEntity>(string identifier
9597
return this;
9698
}
9799

100+
public IOperatorBuilder AddCrdInstaller(Action<CrdInstallerSettings>? configure = null)
101+
{
102+
var settings = new CrdInstallerSettings();
103+
configure?.Invoke(settings);
104+
Services.AddSingleton(settings);
105+
Services.AddHostedService<CrdInstaller>();
106+
return this;
107+
}
108+
98109
private void AddOperatorBase()
99110
{
100111
Services.AddSingleton(_settings);
@@ -117,8 +128,8 @@ private void AddOperatorBase()
117128
Services.TryAddSingleton<IKubernetesClient, KubernetesClient.KubernetesClient>();
118129

119130
Services.TryAddTransient<IEventPublisherFactory, KubeOpsEventPublisherFactory>();
120-
Services.TryAddTransient<EventPublisher>(
121-
services => services.GetRequiredService<IEventPublisherFactory>().Create());
131+
Services.TryAddTransient<EventPublisher>(services =>
132+
services.GetRequiredService<IEventPublisherFactory>().Create());
122133

123134
Services.AddSingleton(typeof(IEntityLabelSelector<>), typeof(DefaultEntityLabelSelector<>));
124135

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Reflection;
2+
using System.Runtime.InteropServices;
3+
4+
using k8s.Models;
5+
6+
using KubeOps.Abstractions.Crds;
7+
using KubeOps.Abstractions.Entities.Attributes;
8+
using KubeOps.KubernetesClient;
9+
using KubeOps.Transpiler;
10+
11+
using Microsoft.Extensions.Hosting;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace KubeOps.Operator.Crds;
15+
16+
internal class CrdInstaller(ILogger<CrdInstaller> logger, CrdInstallerSettings settings, IKubernetesClient client)
17+
: IHostedService
18+
{
19+
private List<V1CustomResourceDefinition> _crds = [];
20+
21+
public async Task StartAsync(CancellationToken cancellationToken)
22+
{
23+
logger.LogInformation("Execute CRD installer with overwrite: {Overwrite}", settings.OverwriteExisting);
24+
var assembly = Assembly.GetEntryAssembly();
25+
if (assembly is null)
26+
{
27+
logger.LogError("No entry assembly found, cannot install CRDs.");
28+
return;
29+
}
30+
31+
var entities = assembly
32+
.DefinedTypes
33+
.Where(t => t is { IsInterface: false, IsAbstract: false, IsGenericType: false })
34+
.Select(t => (t, attrs: CustomAttributeData.GetCustomAttributes(t)))
35+
.Where(e => e.attrs.Any(a => a.AttributeType.Name == nameof(KubernetesEntityAttribute)) &&
36+
e.attrs.All(a => a.AttributeType.Name != nameof(IgnoreAttribute)))
37+
.Select(e => e.t);
38+
39+
using var mlc = ContextCreator.Create(
40+
Directory
41+
.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll")
42+
.Concat(
43+
Directory.GetFiles(Path.GetDirectoryName(assembly.Location)!, "*.dll"))
44+
.Distinct(),
45+
coreAssemblyName: typeof(object).Assembly.GetName().Name);
46+
_crds = mlc.Transpile(entities).ToList();
47+
48+
foreach (var crd in _crds)
49+
{
50+
var existing =
51+
await client.GetAsync<V1CustomResourceDefinition>(crd.Name(), cancellationToken: cancellationToken);
52+
if (existing is not null && !settings.OverwriteExisting)
53+
{
54+
logger.LogDebug("CRD {Name} already exists, skipping installation.", crd.Name());
55+
}
56+
else if (existing is not null)
57+
{
58+
logger.LogDebug("CRD {Name} already exists.", crd.Name());
59+
logger.LogInformation("Overwriting existing CRD {Name}.", crd.Name());
60+
crd.Metadata.ResourceVersion = existing.ResourceVersion();
61+
await client.UpdateAsync(crd, cancellationToken);
62+
}
63+
else
64+
{
65+
logger.LogInformation("Installing CRD {Name}.", crd.Name());
66+
await client.CreateAsync(crd, cancellationToken);
67+
}
68+
}
69+
}
70+
71+
public async Task StopAsync(CancellationToken cancellationToken)
72+
{
73+
if (!settings.DeleteOnShutdown)
74+
{
75+
logger.LogDebug("Skipping CRD deletion on shutdown as per settings.");
76+
return;
77+
}
78+
79+
logger.LogInformation("Deleting CRDs on shutdown.");
80+
foreach (var crd in _crds)
81+
{
82+
try
83+
{
84+
logger.LogInformation("Deleting CRD {Name}.", crd.Name());
85+
await client.DeleteAsync(crd, cancellationToken);
86+
}
87+
catch (Exception ex)
88+
{
89+
logger.LogError(ex, "Failed to delete CRD {Name}.", crd.Name());
90+
}
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)