Skip to content

Commit

Permalink
feat(web operator): add localtunnel feature for easy webhook developm…
Browse files Browse the repository at this point in the history
…ent.
  • Loading branch information
buehler committed Oct 13, 2023
1 parent 5c28a14 commit 0b5ca0a
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 52 deletions.
7 changes: 6 additions & 1 deletion examples/WebhookOperator/Program.cs
@@ -1,9 +1,14 @@
using KubeOps.Operator;
using KubeOps.Operator.Web.Builder;

var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddKubernetesOperator()
.RegisterComponents();
.RegisterComponents()
#if DEBUG
.AddDevelopmentTunnel(5000)
#endif
;

builder.Services
.AddControllers();
Expand Down
4 changes: 2 additions & 2 deletions examples/WebhookOperator/appsettings.json
@@ -1,8 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft.AspNetCore": "Trace",
"Default": "Information",
"Microsoft.AspNetCore": "Information",
"KubeOps": "Trace"
}
},
Expand Down
49 changes: 0 additions & 49 deletions examples/WebhookOperator/webhook_configs.yaml

This file was deleted.

47 changes: 47 additions & 0 deletions src/KubeOps.Operator.Web/Builder/OperatorBuilderExtensions.cs
@@ -0,0 +1,47 @@
using KubeOps.Abstractions.Builder;
using KubeOps.Operator.Web.LocalTunnel;

using Microsoft.Extensions.DependencyInjection;

namespace KubeOps.Operator.Web.Builder;

/// <summary>
/// Method extensions for the operator builder to register web specific services.
/// </summary>
public static class OperatorBuilderExtensions
{
/// <summary>
/// Adds a hosted service to the system that creates a "Local Tunnel"
/// (http://localtunnel.github.io/www/) to the running application.
/// The tunnel points to the configured host/port configuration and then
/// registers itself as webhook target within Kubernetes. This
/// enables developers to easily create webhooks without the requirement
/// of registering ngrok / localtunnel urls themselves.
/// </summary>
/// <param name="builder">The operator builder.</param>
/// <param name="port">The desired port that the asp.net application will run on.</param>
/// <param name="hostname">The desired hostname.</param>
/// <returns>The builder for chaining.</returns>
/// <example>
/// Attach the development tunnel to the operator if in debug mode.
/// <code>
/// var builder = WebApplication.CreateBuilder(args);
/// builder.Services
/// .AddKubernetesOperator()
/// .RegisterComponents()
/// #if DEBUG
/// .AddDevelopmentTunnel(5000)
/// #endif
/// ;
/// </code>
/// </example>
public static IOperatorBuilder AddDevelopmentTunnel(
this IOperatorBuilder builder,
ushort port,
string hostname = "localhost")
{
builder.Services.AddHostedService<DevelopmentTunnelService>();
builder.Services.AddSingleton(new TunnelConfig(hostname, port));
return builder;
}
}
1 change: 1 addition & 0 deletions src/KubeOps.Operator.Web/KubeOps.Operator.Web.csproj
Expand Up @@ -25,6 +25,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Localtunnel" Version="2.0.0-preview.1" />
<PackageReference Include="SystemTextJson.JsonDiffPatch" Version="1.3.1" />
</ItemGroup>

Expand Down
130 changes: 130 additions & 0 deletions src/KubeOps.Operator.Web/LocalTunnel/DevelopmentTunnelService.cs
@@ -0,0 +1,130 @@
using System.Reflection;

using k8s;
using k8s.Models;

using KubeOps.Operator.Client;
using KubeOps.Operator.Web.Webhooks.Mutation;
using KubeOps.Operator.Web.Webhooks.Validation;
using KubeOps.Transpiler;

using Localtunnel;
using Localtunnel.Endpoints.Http;
using Localtunnel.Handlers.Kestrel;
using Localtunnel.Processors;
using Localtunnel.Tunnels;

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace KubeOps.Operator.Web.LocalTunnel;

internal class DevelopmentTunnelService : IHostedService
{
private readonly TunnelConfig _config;
private readonly LocaltunnelClient _tunnelClient;
private Tunnel? _tunnel;

public DevelopmentTunnelService(ILoggerFactory loggerFactory, TunnelConfig config)
{
_config = config;
_tunnelClient = new(loggerFactory);
}

public async Task StartAsync(CancellationToken cancellationToken)
{
_tunnel = await _tunnelClient.OpenAsync(
new KestrelTunnelConnectionHandler(
new HttpRequestProcessingPipelineBuilder()
.Append(new HttpHostHeaderRewritingRequestProcessor(_config.Hostname)).Build(),
new HttpTunnelEndpointFactory(_config.Hostname, _config.Port)),
cancellationToken: cancellationToken);
await _tunnel.StartAsync(cancellationToken: cancellationToken);
await RegisterValidators(_tunnel.Information.Url);
await RegisterMutators(_tunnel.Information.Url);
}

public Task StopAsync(CancellationToken cancellationToken)
{
_tunnel?.Dispose();
return Task.CompletedTask;
}

private static async Task RegisterValidators(Uri uri)
{
var validationWebhooks = Assembly
.GetEntryAssembly()!
.DefinedTypes
.Where(t => t.BaseType?.IsGenericType == true &&
t.BaseType?.GetGenericTypeDefinition() == typeof(ValidationWebhook<>))
.Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(),
Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata))
.Select(hook => new V1ValidatingWebhook
{
Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Rules = new[]
{
new V1RuleWithOperations
{
Operations = new[] { "*" },
Resources = new[] { hook.Metadata.PluralName },
ApiGroups = new[] { hook.Metadata.Group },
ApiVersions = new[] { hook.Metadata.Version },
},
},
ClientConfig = new Admissionregistrationv1WebhookClientConfig
{
Url = $"{uri}validate/{hook.HookTypeName}",
},
});

var validatorConfig = new V1ValidatingWebhookConfiguration(
metadata: new V1ObjectMeta(name: "dev-validators"),
webhooks: validationWebhooks.ToList()).Initialize();

using var validatorClient = KubernetesClientFactory.Create<V1ValidatingWebhookConfiguration>();
await validatorClient.SaveAsync(validatorConfig);
}

private static async Task RegisterMutators(Uri uri)
{
var mutationWebhooks = Assembly
.GetEntryAssembly()!
.DefinedTypes
.Where(t => t.BaseType?.IsGenericType == true &&
t.BaseType?.GetGenericTypeDefinition() == typeof(MutationWebhook<>))
.Select(t => (HookTypeName: t.BaseType!.GenericTypeArguments[0].Name.ToLowerInvariant(),
Entities.ToEntityMetadata(t.BaseType!.GenericTypeArguments[0]).Metadata))
.Select(hook => new V1MutatingWebhook
{
Name = $"validate.{hook.Metadata.SingularName}.{hook.Metadata.Group}.{hook.Metadata.Version}",
MatchPolicy = "Exact",
AdmissionReviewVersions = new[] { "v1" },
SideEffects = "None",
Rules = new[]
{
new V1RuleWithOperations
{
Operations = new[] { "*" },
Resources = new[] { hook.Metadata.PluralName },
ApiGroups = new[] { hook.Metadata.Group },
ApiVersions = new[] { hook.Metadata.Version },
},
},
ClientConfig = new Admissionregistrationv1WebhookClientConfig
{
Url = $"{uri}validate/{hook.HookTypeName}",
},
});

var mutatorConfig = new V1MutatingWebhookConfiguration(
metadata: new V1ObjectMeta(name: "dev-mutators"),
webhooks: mutationWebhooks.ToList()).Initialize();

using var mutatorClient = KubernetesClientFactory.Create<V1MutatingWebhookConfiguration>();
await mutatorClient.SaveAsync(mutatorConfig);
}
}
3 changes: 3 additions & 0 deletions src/KubeOps.Operator.Web/LocalTunnel/TunnelConfig.cs
@@ -0,0 +1,3 @@
namespace KubeOps.Operator.Web.LocalTunnel;

internal record TunnelConfig(string Hostname, ushort Port);

0 comments on commit 0b5ca0a

Please sign in to comment.