Skip to content

Commit 0b5ca0a

Browse files
author
Christoph Bühler
committed
feat(web operator): add localtunnel feature for easy webhook development.
1 parent 5c28a14 commit 0b5ca0a

File tree

7 files changed

+189
-52
lines changed

7 files changed

+189
-52
lines changed

examples/WebhookOperator/Program.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
using KubeOps.Operator;
2+
using KubeOps.Operator.Web.Builder;
23

34
var builder = WebApplication.CreateBuilder(args);
45
builder.Services
56
.AddKubernetesOperator()
6-
.RegisterComponents();
7+
.RegisterComponents()
8+
#if DEBUG
9+
.AddDevelopmentTunnel(5000)
10+
#endif
11+
;
712

813
builder.Services
914
.AddControllers();

examples/WebhookOperator/appsettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"Logging": {
33
"LogLevel": {
4-
"Default": "Trace",
5-
"Microsoft.AspNetCore": "Trace",
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Information",
66
"KubeOps": "Trace"
77
}
88
},

examples/WebhookOperator/webhook_configs.yaml

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using KubeOps.Abstractions.Builder;
2+
using KubeOps.Operator.Web.LocalTunnel;
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace KubeOps.Operator.Web.Builder;
7+
8+
/// <summary>
9+
/// Method extensions for the operator builder to register web specific services.
10+
/// </summary>
11+
public static class OperatorBuilderExtensions
12+
{
13+
/// <summary>
14+
/// Adds a hosted service to the system that creates a "Local Tunnel"
15+
/// (http://localtunnel.github.io/www/) to the running application.
16+
/// The tunnel points to the configured host/port configuration and then
17+
/// registers itself as webhook target within Kubernetes. This
18+
/// enables developers to easily create webhooks without the requirement
19+
/// of registering ngrok / localtunnel urls themselves.
20+
/// </summary>
21+
/// <param name="builder">The operator builder.</param>
22+
/// <param name="port">The desired port that the asp.net application will run on.</param>
23+
/// <param name="hostname">The desired hostname.</param>
24+
/// <returns>The builder for chaining.</returns>
25+
/// <example>
26+
/// Attach the development tunnel to the operator if in debug mode.
27+
/// <code>
28+
/// var builder = WebApplication.CreateBuilder(args);
29+
/// builder.Services
30+
/// .AddKubernetesOperator()
31+
/// .RegisterComponents()
32+
/// #if DEBUG
33+
/// .AddDevelopmentTunnel(5000)
34+
/// #endif
35+
/// ;
36+
/// </code>
37+
/// </example>
38+
public static IOperatorBuilder AddDevelopmentTunnel(
39+
this IOperatorBuilder builder,
40+
ushort port,
41+
string hostname = "localhost")
42+
{
43+
builder.Services.AddHostedService<DevelopmentTunnelService>();
44+
builder.Services.AddSingleton(new TunnelConfig(hostname, port));
45+
return builder;
46+
}
47+
}

src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
</ItemGroup>
2626

2727
<ItemGroup>
28+
<PackageReference Include="Localtunnel" Version="2.0.0-preview.1" />
2829
<PackageReference Include="SystemTextJson.JsonDiffPatch" Version="1.3.1" />
2930
</ItemGroup>
3031

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System.Reflection;
2+
3+
using k8s;
4+
using k8s.Models;
5+
6+
using KubeOps.Operator.Client;
7+
using KubeOps.Operator.Web.Webhooks.Mutation;
8+
using KubeOps.Operator.Web.Webhooks.Validation;
9+
using KubeOps.Transpiler;
10+
11+
using Localtunnel;
12+
using Localtunnel.Endpoints.Http;
13+
using Localtunnel.Handlers.Kestrel;
14+
using Localtunnel.Processors;
15+
using Localtunnel.Tunnels;
16+
17+
using Microsoft.Extensions.Hosting;
18+
using Microsoft.Extensions.Logging;
19+
20+
namespace KubeOps.Operator.Web.LocalTunnel;
21+
22+
internal class DevelopmentTunnelService : IHostedService
23+
{
24+
private readonly TunnelConfig _config;
25+
private readonly LocaltunnelClient _tunnelClient;
26+
private Tunnel? _tunnel;
27+
28+
public DevelopmentTunnelService(ILoggerFactory loggerFactory, TunnelConfig config)
29+
{
30+
_config = config;
31+
_tunnelClient = new(loggerFactory);
32+
}
33+
34+
public async Task StartAsync(CancellationToken cancellationToken)
35+
{
36+
_tunnel = await _tunnelClient.OpenAsync(
37+
new KestrelTunnelConnectionHandler(
38+
new HttpRequestProcessingPipelineBuilder()
39+
.Append(new HttpHostHeaderRewritingRequestProcessor(_config.Hostname)).Build(),
40+
new HttpTunnelEndpointFactory(_config.Hostname, _config.Port)),
41+
cancellationToken: cancellationToken);
42+
await _tunnel.StartAsync(cancellationToken: cancellationToken);
43+
await RegisterValidators(_tunnel.Information.Url);
44+
await RegisterMutators(_tunnel.Information.Url);
45+
}
46+
47+
public Task StopAsync(CancellationToken cancellationToken)
48+
{
49+
_tunnel?.Dispose();
50+
return Task.CompletedTask;
51+
}
52+
53+
private static async Task RegisterValidators(Uri uri)
54+
{
55+
var validationWebhooks = Assembly
56+
.GetEntryAssembly()!
57+
.DefinedTypes
58+
.Where(t => t.BaseType?.IsGenericType == true &&
59+
t.BaseType?.GetGenericTypeDefinition() == typeof(ValidationWebhook<>))
60+
.Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(),
61+
Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata))
62+
.Select(hook => new V1ValidatingWebhook
63+
{
64+
Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
65+
MatchPolicy = "Exact",
66+
AdmissionReviewVersions = new[] { "v1" },
67+
SideEffects = "None",
68+
Rules = new[]
69+
{
70+
new V1RuleWithOperations
71+
{
72+
Operations = new[] { "*" },
73+
Resources = new[] { hook.Metadata.PluralName },
74+
ApiGroups = new[] { hook.Metadata.Group },
75+
ApiVersions = new[] { hook.Metadata.Version },
76+
},
77+
},
78+
ClientConfig = new Admissionregistrationv1WebhookClientConfig
79+
{
80+
Url = $"{uri}validate/{hook.HookTypeName}",
81+
},
82+
});
83+
84+
var validatorConfig = new V1ValidatingWebhookConfiguration(
85+
metadata: new V1ObjectMeta(name: "dev-validators"),
86+
webhooks: validationWebhooks.ToList()).Initialize();
87+
88+
using var validatorClient = KubernetesClientFactory.Create<V1ValidatingWebhookConfiguration>();
89+
await validatorClient.SaveAsync(validatorConfig);
90+
}
91+
92+
private static async Task RegisterMutators(Uri uri)
93+
{
94+
var mutationWebhooks = Assembly
95+
.GetEntryAssembly()!
96+
.DefinedTypes
97+
.Where(t => t.BaseType?.IsGenericType == true &&
98+
t.BaseType?.GetGenericTypeDefinition() == typeof(MutationWebhook<>))
99+
.Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(),
100+
Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata))
101+
.Select(hook => new V1MutatingWebhook
102+
{
103+
Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
104+
MatchPolicy = "Exact",
105+
AdmissionReviewVersions = new[] { "v1" },
106+
SideEffects = "None",
107+
Rules = new[]
108+
{
109+
new V1RuleWithOperations
110+
{
111+
Operations = new[] { "*" },
112+
Resources = new[] { hook.Metadata.PluralName },
113+
ApiGroups = new[] { hook.Metadata.Group },
114+
ApiVersions = new[] { hook.Metadata.Version },
115+
},
116+
},
117+
ClientConfig = new Admissionregistrationv1WebhookClientConfig
118+
{
119+
Url = $"{uri}validate/{hook.HookTypeName}",
120+
},
121+
});
122+
123+
var mutatorConfig = new V1MutatingWebhookConfiguration(
124+
metadata: new V1ObjectMeta(name: "dev-mutators"),
125+
webhooks: mutationWebhooks.ToList()).Initialize();
126+
127+
using var mutatorClient = KubernetesClientFactory.Create<V1MutatingWebhookConfiguration>();
128+
await mutatorClient.SaveAsync(mutatorConfig);
129+
}
130+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace KubeOps.Operator.Web.LocalTunnel;
2+
3+
internal record TunnelConfig(string Hostname, ushort Port);

0 commit comments

Comments
 (0)