This commit is contained in:
2024-02-20 17:15:27 +08:00
committed by huty
parent 6706e1a633
commit 34158042ad
1529 changed files with 177765 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.301-alpine AS builder
WORKDIR /src
COPY src/UserController.csproj .
RUN dotnet restore
COPY src/ .
RUN dotnet publish -c Release -o /out UserController.csproj
# app image
FROM mcr.microsoft.com/dotnet/core/runtime:3.1.5-alpine
WORKDIR /app
ENTRYPOINT ["dotnet", "UserController.dll"]
COPY --from=builder /out/ .

View File

@@ -0,0 +1,8 @@
## Credits
https://radu-matei.com/blog/kubernetes-controller-csharp/
https://github.com/engineerd/kubecontroller-csharp
https://fearofoblivion.com/intro-to-kubernetes-custom-resource-definitions-or-crds

View File

@@ -0,0 +1,35 @@
using k8s;
using k8s.Models;
using Newtonsoft.Json;
using System.Text.Json.Serialization;
namespace UserController.CustomResources
{
public class User : KubernetesObject
{
public V1ObjectMeta Metadata { get; set; }
public UserSpec Spec { get; set; }
public class UserSpec
{
public string Email { get; set; }
public string Group { get; set; }
public string Token { get; set; }
public string GetGroupNamespace()
{
return $"kiamol-ch20-authn-{Group}";
}
}
public struct Definition
{
public const string Group = "ch20.kiamol.net";
public const string Version= "v1";
public const string Plural = "users";
}
}
}

View File

@@ -0,0 +1,118 @@
using AutoMapper;
using k8s;
using k8s.Models;
using Microsoft.AspNetCore.JsonPatch;
using Org.BouncyCastle.Crypto.Tls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UserController.CustomResources;
namespace UserController.Handlers
{
public class UserAddedHandler
{
private readonly Kubernetes _client;
public UserAddedHandler(Kubernetes client)
{
_client = client;
}
public void Handle(User user)
{
EnsureGroupNamespace(user);
EnsureServiceAccount(user);
EnsureServiceAccountToken(user);
}
private void EnsureGroupNamespace(User user)
{
var groupNamespaceName = user.Spec.GetGroupNamespace();
var kiamolNamespaces = _client.ListNamespace(fieldSelector: $"metadata.name={groupNamespaceName}");
if (!kiamolNamespaces.Items.Any())
{
var groupNamespace = new V1Namespace
{
Metadata = new V1ObjectMeta
{
Name = groupNamespaceName,
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20"},
}
}
};
_client.CreateNamespace(groupNamespace);
Console.WriteLine($"** Created group namespace: {groupNamespaceName}");
}
else
{
Console.WriteLine($"** Group namespace exists: {groupNamespaceName}");
}
}
private void EnsureServiceAccount(User user)
{
var groupNamespaceName = user.Spec.GetGroupNamespace();
var serviceAccountName = user.Metadata.Name;
var serviceAccounts = _client.ListNamespacedServiceAccount(groupNamespaceName,
fieldSelector: $"metadata.name={serviceAccountName}");
if (!serviceAccounts.Items.Any())
{
var serviceAccount = new V1ServiceAccount
{
Metadata = new V1ObjectMeta
{
Name = serviceAccountName,
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20"},
}
},
AutomountServiceAccountToken = false
};
_client.CreateNamespacedServiceAccount(serviceAccount, groupNamespaceName);
Console.WriteLine($"** Created service account: {serviceAccountName}, in group namespace: {groupNamespaceName}");
}
else
{
Console.WriteLine($"** Service account exists: {serviceAccountName}, in group namespace: {groupNamespaceName}");
}
}
private void EnsureServiceAccountToken(User user)
{
var groupNamespaceName = user.Spec.GetGroupNamespace();
var tokenName = $"{user.Metadata.Name}-token";
var tokens = _client.ListNamespacedSecret(groupNamespaceName,
fieldSelector: $"metadata.name={tokenName}");
if (!tokens.Items.Any())
{
var secret = new V1Secret
{
Metadata = new V1ObjectMeta
{
Name = tokenName,
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20"},
},
Annotations = new Dictionary<string, string>()
{
{ "kubernetes.io/service-account.name", user.Metadata.Name},
}
},
Type = "kubernetes.io/service-account-token"
};
_client.CreateNamespacedSecret(secret, groupNamespaceName);
Console.WriteLine($"** Created token: {tokenName}, in group namespace: {groupNamespaceName}");
}
else
{
Console.WriteLine($"** Token exists: {tokenName}, in group namespace: {groupNamespaceName}");
}
}
}
}

View File

@@ -0,0 +1,55 @@
using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UserController.CustomResources;
namespace UserController.Handlers
{
class UserDeletedHandler
{
// delete sa
// delete group ns if 0 sas
private readonly Kubernetes _client;
public UserDeletedHandler(Kubernetes client)
{
_client = client;
}
public void Handle(User user)
{
DeleteServiceAccount(user);
DeleteNamespaceIfEmpty(user);
}
private void DeleteServiceAccount(User user)
{
var groupNamespaceName = user.Spec.GetGroupNamespace();
var serviceAccountName = user.Metadata.Name;
var serviceAccounts = _client.ListNamespacedServiceAccount(groupNamespaceName,
fieldSelector: $"metadata.name={serviceAccountName}");
if (serviceAccounts.Items.Any())
{
_client.DeleteNamespacedServiceAccount(serviceAccountName, groupNamespaceName);
Console.WriteLine($"** Deleted service account: {serviceAccountName}, in group namespace: {groupNamespaceName}");
}
}
private void DeleteNamespaceIfEmpty(User user)
{
var groupNamespaceName = user.Spec.GetGroupNamespace();
var serviceAccountName = user.Metadata.Name;
var serviceAccounts = _client.ListNamespacedServiceAccount(groupNamespaceName);
if (serviceAccounts.Items.Count == 1 && serviceAccounts.Items[0].Metadata.Name == "default")
{
_client.DeleteNamespace(groupNamespaceName);
Console.WriteLine($"** No accounts left, deleted namespace: {groupNamespaceName}");
}
}
}
}

View File

@@ -0,0 +1,64 @@
using k8s;
using System;
using System.Threading;
using System.Threading.Tasks;
using UserController.CustomResources;
using UserController.Handlers;
namespace UserController
{
class Program
{
private static ManualResetEvent _ResetEvent = new ManualResetEvent(false);
private static Kubernetes _Client;
static async Task Main(string[] args)
{
KubernetesClientConfiguration config;
if (KubernetesClientConfiguration.IsInCluster())
{
config = KubernetesClientConfiguration.InClusterConfig();
}
else
{
config = new KubernetesClientConfiguration { Host = "http://localhost:8001" };
}
_Client = new Kubernetes(config);
var result = await _Client.ListNamespacedCustomObjectWithHttpMessagesAsync(
group: User.Definition.Group,
version: User.Definition.Version,
plural: User.Definition.Plural,
namespaceParameter: "default",
watch: true);
using (result.Watch<User, object>((type, item) => Handle(type, item)))
{
Console.WriteLine("* Watching for custom object events");
_ResetEvent.WaitOne();
}
}
public static void Handle(WatchEventType type, User user)
{
switch (type)
{
case WatchEventType.Added:
new UserAddedHandler(_Client).Handle(user);
Console.WriteLine($"* Handled event: {type}, for user: {user.Metadata.Name}");
break;
case WatchEventType.Deleted:
new UserDeletedHandler(_Client).Handle(user);
Console.WriteLine($"* Handled event: {type}, for user: {user.Metadata.Name}");
break;
default:
Console.WriteLine($"* Ignored event: {type}, for user: {user.Metadata.Name}");
break;
}
}
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="2.0.26" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30309.148
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserController", "UserController.csproj", "{D92E46DD-C089-48E5-8B94-371EC9A27704}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D92E46DD-C089-48E5-8B94-371EC9A27704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D92E46DD-C089-48E5-8B94-371EC9A27704}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D92E46DD-C089-48E5-8B94-371EC9A27704}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D92E46DD-C089-48E5-8B94-371EC9A27704}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB8F2AD8-5410-4F5E-AFC6-D8AA6BCDAAC4}
EndGlobalSection
EndGlobal