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,19 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.301-alpine AS builder
WORKDIR /src
COPY src/WebPingOperator.Model/WebPingOperator.Model.csproj ./WebPingOperator.Model/
COPY src/WebPingOperator.WebPingerArchiveController/WebPingOperator.WebPingerArchiveController.csproj ./WebPingOperator.WebPingerArchiveController/
WORKDIR /src/WebPingOperator.WebPingerArchiveController
RUN dotnet restore
COPY src /src
RUN dotnet publish -c Release -o /out WebPingOperator.WebPingerArchiveController.csproj
# app image
FROM mcr.microsoft.com/dotnet/core/runtime:3.1.5-alpine
WORKDIR /app
ENTRYPOINT ["dotnet", "WebPingOperator.WebPingerArchiveController.dll"]
COPY --from=builder /out/ .

View File

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

View File

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

View File

@@ -0,0 +1,105 @@
using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
namespace WebPingOperator.Installer.Installers
{
public class WebPingerArchiveInstaller : IInstaller
{
private readonly Kubernetes _kubernetes;
public WebPingerArchiveInstaller(Kubernetes kubernetes)
{
_kubernetes = kubernetes;
}
public async Task InstallAsync()
{
await EnsureWebPingerArchiveCrdAsync();
}
private async Task EnsureWebPingerArchiveCrdAsync()
{
var crds = await _kubernetes.ListCustomResourceDefinitionAsync(
fieldSelector: $"metadata.name={WebPingerArchive.Definition.Plural}.{WebPinger.Definition.Group}");
if (!crds.Items.Any())
{
var crd = new V1CustomResourceDefinition
{
Metadata = new V1ObjectMeta
{
Name = $"{WebPingerArchive.Definition.Plural}.{WebPingerArchive.Definition.Group}",
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20" },
{ "operator", "web-ping" }
}
},
Spec = new V1CustomResourceDefinitionSpec
{
Group = WebPingerArchive.Definition.Group,
Scope = "Namespaced",
Names = new V1CustomResourceDefinitionNames
{
Plural = WebPingerArchive.Definition.Plural,
Singular = WebPingerArchive.Definition.Singular,
Kind = WebPingerArchive.Definition.Kind,
ShortNames = new List<string>
{
WebPingerArchive.Definition.ShortName
}
},
Versions = new List<V1CustomResourceDefinitionVersion>
{
new V1CustomResourceDefinitionVersion
{
Name = "v1",
Served = true,
Storage = true,
Schema = new V1CustomResourceValidation
{
OpenAPIV3Schema = new V1JSONSchemaProps
{
Type = "object",
Properties = new Dictionary<string, V1JSONSchemaProps>
{
{
"spec",
new V1JSONSchemaProps
{
Type = "object",
Properties = new Dictionary<string, V1JSONSchemaProps>
{
{
"target",
new V1JSONSchemaProps
{
Type = "string"
}
}
}
}
}
}
}
}
}
}
}
};
await _kubernetes.CreateCustomResourceDefinitionAsync(crd);
Console.WriteLine($"** Created CRD for Kind: {WebPingerArchive.Definition.Kind}; ApiVersion: {WebPinger.Definition.Group}/{WebPinger.Definition.Version}");
}
else
{
Console.WriteLine($"** CRD already exists for Kind: {WebPingerArchive.Definition.Kind}; ApiVersion: {WebPinger.Definition.Group}/{WebPinger.Definition.Version}");
}
}
}
}

View File

@@ -0,0 +1,119 @@
using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
namespace WebPingOperator.Installer.Installers
{
public class WebPingerInstaller : IInstaller
{
private readonly Kubernetes _kubernetes;
public WebPingerInstaller(Kubernetes kubernetes)
{
_kubernetes = kubernetes;
}
public async Task InstallAsync()
{
await EnsureWebPingerCrdAsync();
}
private async Task EnsureWebPingerCrdAsync()
{
var crds = await _kubernetes.ListCustomResourceDefinitionAsync(
fieldSelector: $"metadata.name={WebPinger.Definition.Plural}.{WebPinger.Definition.Group}");
if (!crds.Items.Any())
{
var crd = new V1CustomResourceDefinition
{
Metadata = new V1ObjectMeta
{
Name = $"{WebPinger.Definition.Plural}.{WebPinger.Definition.Group}",
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20" },
{ "operator", "web-ping" }
}
},
Spec = new V1CustomResourceDefinitionSpec
{
Group = WebPinger.Definition.Group,
Scope = "Namespaced",
Names = new V1CustomResourceDefinitionNames
{
Plural = WebPinger.Definition.Plural,
Singular = WebPinger.Definition.Singular,
Kind = WebPinger.Definition.Kind,
ShortNames = new List<string>
{
WebPinger.Definition.ShortName
}
},
Versions = new List<V1CustomResourceDefinitionVersion>
{
new V1CustomResourceDefinitionVersion
{
Name = "v1",
Served = true,
Storage = true,
Schema = new V1CustomResourceValidation
{
OpenAPIV3Schema = new V1JSONSchemaProps
{
Type = "object",
Properties = new Dictionary<string, V1JSONSchemaProps>
{
{
"spec",
new V1JSONSchemaProps
{
Type = "object",
Properties = new Dictionary<string, V1JSONSchemaProps>
{
{
"target",
new V1JSONSchemaProps
{
Type = "string"
}
},
{
"interval",
new V1JSONSchemaProps
{
Type = "string"
}
},
{
"method",
new V1JSONSchemaProps
{
Type = "string"
}
}
}
}
}
}
}
}
}
}
}
};
await _kubernetes.CreateCustomResourceDefinitionAsync(crd);
Console.WriteLine($"** Created CRD for Kind: {WebPinger.Definition.Kind}; ApiVersion: {WebPinger.Definition.Group}/{WebPinger.Definition.Version}");
}
else
{
Console.WriteLine($"** CRD already exists for Kind: {WebPinger.Definition.Kind}; ApiVersion: {WebPinger.Definition.Group}/{WebPinger.Definition.Version}");
}
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace WebPingOperator.Installer.Installers
{
public interface IInstaller
{
Task InstallAsync();
}
}

View File

@@ -0,0 +1,40 @@
using k8s;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
using WebPingOperator.Installer.Installers;
namespace WebPingOperator.Installer
{
class Program
{
static async Task Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddJsonFile("config/appsettings.json", optional: true)
.Build();
var kubeConfig = KubernetesClientConfiguration.IsInCluster() ?
KubernetesClientConfiguration.InClusterConfig() :
new KubernetesClientConfiguration { Host = "http://localhost:8001" };
var kubernetes = new Kubernetes(kubeConfig);
var serviceProvider = new ServiceCollection()
.AddSingleton(config)
.AddSingleton(kubernetes)
.AddTransient<IInstaller, WebPingerInstaller>()
.AddTransient<IInstaller, WebPingerArchiveInstaller>()
.BuildServiceProvider();
var installers = serviceProvider.GetServices<IInstaller>();
foreach (var installer in installers)
{
await installer.InstallAsync();
}
Console.WriteLine("* Done.");
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="2.0.26" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WebPingOperator.Model\WebPingOperator.Model.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
namespace WebPingOperator.Model.CustomResources
{
public class WebPinger : KubernetesObject
{
public V1ObjectMeta Metadata { get; set; }
public WebPingSpec Spec { get; set; }
public class WebPingSpec
{
public string Target { get; set; }
public string Interval { get; set; }
public string Method { get; set; }
public int GetIntervalMilliseconds()
{
var interval = int.Parse(Interval[0..^1]);
var measure = Interval.ToLower()[^1];
if (measure == 'm')
{
interval *= 60;
}
//otherwise assume seconds
return interval * 1000;
}
public string GetMethod()
{
return Method ?? "HEAD";
}
}
public struct Definition
{
public const string Group = "ch20.kiamol.net";
public const string Version = "v1";
public const string Plural = "webpingers";
public const string Singular = "webpinger";
public const string Kind = "WebPinger";
public const string ShortName = "wp";
}
}
}

View File

@@ -0,0 +1,28 @@
using k8s;
using k8s.Models;
using System;
namespace WebPingOperator.Model.CustomResources
{
public class WebPingerArchive : KubernetesObject
{
public V1ObjectMeta Metadata { get; set; }
public WebPingSpec Spec { get; set; }
public class WebPingSpec
{
public string Target { get; set; }
}
public struct Definition
{
public const string Group = "ch20.kiamol.net";
public const string Version = "v1";
public const string Plural = "webpingerarchives";
public const string Singular = "webpingerarchive";
public const string Kind = "WebPingerArchive";
public const string ShortName = "wpa";
}
}
}

View File

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

View File

@@ -0,0 +1,105 @@
using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
namespace WebPingOperator.WebPingerArchiveController.Handlers
{
public class WebPingerArchiveAddedHandler
{
private readonly Kubernetes _kubernetes;
public WebPingerArchiveAddedHandler(Kubernetes kubernetes)
{
_kubernetes = kubernetes;
}
public async Task<bool> HandleAsync(WebPingerArchive archive)
{
await CreateJobAsync(archive);
return true;
}
private async Task<bool> CreateJobAsync(WebPingerArchive archive)
{
// check a pinger exists to archive:
var services = await _kubernetes.ListNamespacedServiceAsync(
Program.NamespaceName,
labelSelector: $"app=web-ping,target={archive.Spec.Target}");
if (!services.Items.Any())
{
Console.WriteLine($"** No WebPinger Service exists for target: {archive.Spec.Target}, in namespace: {Program.NamespaceName}");
return false;
}
var pingerServiceName = services.Items.First().Metadata.Name;
var name = $"wpa-{archive.Metadata.Name}-{archive.Metadata.CreationTimestamp:yyMMdd-HHmm}";
var jobs = await _kubernetes.ListNamespacedJobAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (!jobs.Items.Any())
{
var job = new V1Job
{
Metadata = new V1ObjectMeta
{
Name = name,
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20" },
}
},
Spec = new V1JobSpec
{
Completions = 1,
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta
{
Labels = new Dictionary<string, string>()
{
{ "app", "web-ping-archive"},
{ "target", archive.Spec.Target }
}
},
Spec = new V1PodSpec
{
AutomountServiceAccountToken = false,
RestartPolicy = "Never",
Containers = new List<V1Container>
{
new V1Container
{
Name = "archiver",
Image = "kiamol/ch20-web-ping-archiver",
Env = new List<V1EnvVar>
{
new V1EnvVar
{
Name = "WEB_PING_URL",
Value = $"http://{pingerServiceName}:8080/archive"
}
}
}
}
}
}
}
};
await _kubernetes.CreateNamespacedJobAsync(job, Program.NamespaceName);
Console.WriteLine($"** Created Job: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** Job exists: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
}
}

View File

@@ -0,0 +1,44 @@
using k8s;
using System;
using System.Linq;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
namespace WebPingOperator.WebPingerArchiveController.Handlers
{
public class WebPingerArchiveDeletedHandler
{
private readonly Kubernetes _kubernetes;
public WebPingerArchiveDeletedHandler(Kubernetes kubernetes)
{
_kubernetes = kubernetes;
}
public async Task<bool> HandleAsync(WebPingerArchive archive)
{
await DeleteJobAsync(archive);
return true;
}
private async Task<bool> DeleteJobAsync(WebPingerArchive archive)
{
var name = $"wpa-{archive.Metadata.Name}-{archive.Metadata.CreationTimestamp:yyMMdd-HHmm}";
var jobs = await _kubernetes.ListNamespacedJobAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (jobs.Items.Any())
{
await _kubernetes.DeleteNamespacedJobAsync(name, Program.NamespaceName);
Console.WriteLine($"** Deleted Job: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** No Job to delete: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
}
}

View File

@@ -0,0 +1,78 @@
using k8s;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
using WebPingOperator.WebPingerArchiveController.Handlers;
namespace WebPingOperator.WebPingerArchiveController
{
class Program
{
private static ServiceProvider _ServiceProvider;
static async Task Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddJsonFile("config/appsettings.json", optional: true)
.Build();
var kubeConfig = KubernetesClientConfiguration.IsInCluster() ?
KubernetesClientConfiguration.InClusterConfig() :
new KubernetesClientConfiguration { Host = "http://localhost:8001" };
var kubernetes = new Kubernetes(kubeConfig);
_ServiceProvider = new ServiceCollection()
.AddSingleton(config)
.AddSingleton(kubernetes)
.AddTransient<WebPingerArchiveAddedHandler>()
.AddTransient<WebPingerArchiveDeletedHandler>()
.BuildServiceProvider();
// TODO - this hangs and then fails if there are no objects
var result = await kubernetes.ListNamespacedCustomObjectWithHttpMessagesAsync(
group: WebPingerArchive.Definition.Group,
version: WebPingerArchive.Definition.Version,
plural: WebPingerArchive.Definition.Plural,
namespaceParameter: "default",
watch: true);
using (result.Watch<WebPingerArchive, object>(async (type, item) => await Handle(type, item)))
{
Console.WriteLine("* Watching for WebPingerArchive events");
var ctrlc = new ManualResetEventSlim(false);
Console.CancelKeyPress += (sender, eventArgs) => ctrlc.Set();
ctrlc.Wait();
}
}
public static async Task Handle(WatchEventType type, WebPingerArchive archive)
{
switch (type)
{
case WatchEventType.Added:
var addedhandler = _ServiceProvider.GetService<WebPingerArchiveAddedHandler>();
await addedhandler.HandleAsync(archive);
Console.WriteLine($"* Handled event: {type}, for: {archive.Metadata.Name}");
break;
case WatchEventType.Deleted:
var deletedHandler = _ServiceProvider.GetService<WebPingerArchiveDeletedHandler>();
await deletedHandler.HandleAsync(archive);
Console.WriteLine($"* Handled event: {type}, for: {archive.Metadata.Name}");
break;
default:
Console.WriteLine($"* Ignored event: {type}, for: {archive.Metadata.Name}");
break;
}
}
// TODO - move to config:
public const string NamespaceName = "default";
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="2.0.26" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WebPingOperator.Model\WebPingOperator.Model.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,87 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace WebPingOperator.WebPingerController {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class ConfigMapData {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal ConfigMapData() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WebPingOperator.WebPingerController.ConfigMapData", typeof(ConfigMapData).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to const { format, transports } = require(&apos;winston&apos;);
/// var logConfig = module.exports = {};
///
/// logConfig.options = {
/// transports: [
/// new transports.Console({
/// level: &apos;debug&apos;,
/// format: format.combine(
/// format.splat(),
/// format.printf(log =&gt; {
/// return `${log.message}`
/// })
/// )
/// }),
/// new transports.File({
/// level: &apos;debu [rest of string was truncated]&quot;;.
/// </summary>
internal static string logConfig {
get {
return ResourceManager.GetString("logConfig", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="logConfig" xml:space="preserve">
<value>const { format, transports } = require('winston');
var logConfig = module.exports = {};
logConfig.options = {
transports: [
new transports.Console({
level: 'debug',
format: format.combine(
format.splat(),
format.printf(log =&gt; {
return `${log.message}`
})
)
}),
new transports.File({
level: 'debug',
format: format.combine(
format.splat(),
format.timestamp(),
format.json()
),
filename: '/logs/web-ping.log',
maxsize: 104857600, // 100MB
maxFiles: 1,
maxRetries: 10
})
]
};</value>
</data>
</root>

View File

@@ -0,0 +1,253 @@
using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
namespace WebPingOperator.WebPingerController.Handlers
{
public class WebPingerAddedHandler
{
private readonly Kubernetes _kubernetes;
public WebPingerAddedHandler(Kubernetes kubernetes)
{
_kubernetes = kubernetes;
}
public async Task<bool> HandleAsync(WebPinger pinger)
{
await EnsureConfigMapAsync(pinger);
await EnsureDeploymentAsync(pinger);
await EnsureServiceAsync(pinger);
return true;
}
private async Task<bool> EnsureConfigMapAsync(WebPinger pinger)
{
var name = $"wp-{pinger.Metadata.Name}-config";
var configMaps = await _kubernetes.ListNamespacedConfigMapAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (!configMaps.Items.Any())
{
var configMap = new V1ConfigMap
{
Metadata = new V1ObjectMeta
{
Name = name,
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20" },
}
},
Data = new Dictionary<string, string>
{
{ "logConfig.js", ConfigMapData.logConfig.Replace("\r\n", "\n") }
}
};
await _kubernetes.CreateNamespacedConfigMapAsync(configMap, Program.NamespaceName);
Console.WriteLine($"** Created ConfigMap: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** ConfigMap exists: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
private async Task<bool> EnsureDeploymentAsync(WebPinger pinger)
{
var name = $"wp-{pinger.Metadata.Name}";
var deployments = await _kubernetes.ListNamespacedDeploymentAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (!deployments.Items.Any())
{
var deployment = new V1Deployment
{
Metadata = new V1ObjectMeta
{
Name = name,
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20" },
}
},
Spec = new V1DeploymentSpec
{
Selector = new V1LabelSelector
{
MatchLabels = new Dictionary<string, string>()
{
{ "app", "web-ping"},
{ "instance", name }
}
},
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta
{
Labels = new Dictionary<string, string>()
{
{ "app", "web-ping"},
{ "instance", name },
{ "target", pinger.Spec.Target }
}
},
Spec = new V1PodSpec
{
AutomountServiceAccountToken = false,
Containers = new List<V1Container>
{
new V1Container
{
Name = "web",
Image = "kiamol/ch10-web-ping",
Env = new List<V1EnvVar>
{
new V1EnvVar
{
Name = "INTERVAL",
Value = pinger.Spec.GetIntervalMilliseconds().ToString()
},
new V1EnvVar
{
Name = "TARGET",
Value = pinger.Spec.Target
},
new V1EnvVar
{
Name = "METHOD",
Value = pinger.Spec.GetMethod()
}
},
VolumeMounts = new List<V1VolumeMount>
{
new V1VolumeMount
{
Name= "config",
MountPath = "/app/config",
ReadOnlyProperty = true
},
new V1VolumeMount
{
Name= "logs",
MountPath = "/logs"
}
}
},
new V1Container
{
Name = "api",
Image = "kiamol/ch20-log-archiver",
Ports = new List<V1ContainerPort>
{
new V1ContainerPort
{
Name = "api",
ContainerPort = 80
}
},
VolumeMounts = new List<V1VolumeMount>
{
new V1VolumeMount
{
Name= "logs",
MountPath = "/logs"
}
}
}
},
Volumes = new List<V1Volume>
{
new V1Volume
{
Name = "config",
ConfigMap = new V1ConfigMapVolumeSource
{
Name = $"{name}-config"
}
},
new V1Volume
{
Name = "logs",
EmptyDir = new V1EmptyDirVolumeSource()
}
}
}
}
}
};
await _kubernetes.CreateNamespacedDeploymentAsync(deployment, Program.NamespaceName);
Console.WriteLine($"** Created Deployment: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** Deployment exists: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
private async Task<bool> EnsureServiceAsync(WebPinger pinger)
{
var name = $"wp-{pinger.Metadata.Name}";
var services = await _kubernetes.ListNamespacedServiceAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (!services.Items.Any())
{
var service = new V1Service
{
Metadata = new V1ObjectMeta
{
Name = name,
Labels = new Dictionary<string, string>()
{
{ "kiamol", "ch20"},
{ "app", "web-ping"},
{ "target", pinger.Spec.Target }
}
},
Spec = new V1ServiceSpec
{
Type = "ClusterIP",
Ports = new List<V1ServicePort>()
{
new V1ServicePort
{
Port = 8080,
TargetPort = "api"
}
},
Selector = new Dictionary<string, string>()
{
{ "app", "web-ping"},
{ "instance", name }
}
}
};
await _kubernetes.CreateNamespacedServiceAsync(service, Program.NamespaceName);
Console.WriteLine($"** Created Service: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** Service exists: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
}
}

View File

@@ -0,0 +1,87 @@
using k8s;
using System;
using System.Linq;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
namespace WebPingOperator.WebPingerController.Handlers
{
public class WebPingerDeletedHandler
{
private readonly Kubernetes _kubernetes;
public WebPingerDeletedHandler(Kubernetes kubernetes)
{
_kubernetes = kubernetes;
}
public async Task<bool> HandleAsync(WebPinger pinger)
{
await DeleteServiceAsync(pinger);
await DeleteDeploymentAsync(pinger);
await DeleteConfigMapAsync(pinger);
return true;
}
private async Task<bool> DeleteServiceAsync(WebPinger pinger)
{
var name = $"wp-{pinger.Metadata.Name}";
var services = await _kubernetes.ListNamespacedServiceAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (services.Items.Any())
{
await _kubernetes.DeleteNamespacedServiceAsync(name, Program.NamespaceName);
Console.WriteLine($"** Deleted Service: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** No Service to delete: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
private async Task<bool> DeleteDeploymentAsync(WebPinger pinger)
{
var name = $"wp-{pinger.Metadata.Name}";
var deployments = await _kubernetes.ListNamespacedDeploymentAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (deployments.Items.Any())
{
await _kubernetes.DeleteNamespacedDeploymentAsync(name, Program.NamespaceName);
Console.WriteLine($"** Deleted Deployment: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** No Deployment to delete: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
private async Task<bool> DeleteConfigMapAsync(WebPinger pinger)
{
var name = $"wp-{pinger.Metadata.Name}-config";
var configMaps = await _kubernetes.ListNamespacedConfigMapAsync(
Program.NamespaceName,
fieldSelector: $"metadata.name={name}");
if (configMaps.Items.Any())
{
await _kubernetes.DeleteNamespacedConfigMapAsync(name, Program.NamespaceName);
Console.WriteLine($"** Deleted ConfigMap: {name}, in namespace: {Program.NamespaceName}");
return true;
}
else
{
Console.WriteLine($"** No ConfigMap to delete: {name}, in namespace: {Program.NamespaceName}");
return false;
}
}
}
}

View File

@@ -0,0 +1,78 @@
using k8s;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading;
using System.Threading.Tasks;
using WebPingOperator.Model.CustomResources;
using WebPingOperator.WebPingerController.Handlers;
namespace WebPingOperator.WebPingerController
{
class Program
{
private static ServiceProvider _ServiceProvider;
static async Task Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddJsonFile("config/appsettings.json", optional: true)
.Build();
var kubeConfig = KubernetesClientConfiguration.IsInCluster() ?
KubernetesClientConfiguration.InClusterConfig() :
new KubernetesClientConfiguration { Host = "http://localhost:8001" };
var kubernetes = new Kubernetes(kubeConfig);
_ServiceProvider = new ServiceCollection()
.AddSingleton(config)
.AddSingleton(kubernetes)
.AddTransient<WebPingerAddedHandler>()
.AddTransient<WebPingerDeletedHandler>()
.BuildServiceProvider();
// TODO - this hangs and then fails if there are no objects
var result = await kubernetes.ListNamespacedCustomObjectWithHttpMessagesAsync(
group: WebPinger.Definition.Group,
version: WebPinger.Definition.Version,
plural: WebPinger.Definition.Plural,
namespaceParameter: "default",
watch: true);
using (result.Watch<WebPinger, object>(async (type, item) => await Handle(type, item)))
{
Console.WriteLine("* Watching for WebPinger events");
var ctrlc = new ManualResetEventSlim(false);
Console.CancelKeyPress += (sender, eventArgs) => ctrlc.Set();
ctrlc.Wait();
}
}
public static async Task Handle(WatchEventType type, WebPinger pinger)
{
switch (type)
{
case WatchEventType.Added:
var addedhandler = _ServiceProvider.GetService<WebPingerAddedHandler>();
await addedhandler.HandleAsync(pinger);
Console.WriteLine($"* Handled event: {type}, for: {pinger.Metadata.Name}");
break;
case WatchEventType.Deleted:
var deletedHandler = _ServiceProvider.GetService<WebPingerDeletedHandler>();
await deletedHandler.HandleAsync(pinger);
Console.WriteLine($"* Handled event: {type}, for: {pinger.Metadata.Name}");
break;
default:
Console.WriteLine($"* Ignored event: {type}, for: {pinger.Metadata.Name}");
break;
}
}
// TODO - move to config:
public const string NamespaceName = "default";
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="2.0.26" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WebPingOperator.Model\WebPingOperator.Model.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="ConfigMapData.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ConfigMapData.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="ConfigMapData.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>ConfigMapData.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,43 @@

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}") = "WebPingOperator.WebPingerController", ".\WebPingOperator.WebPingerController\WebPingOperator.WebPingerController.csproj", "{27D653BB-04EA-470B-BE5C-A25D78D9DF8A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebPingOperator.Installer", ".\WebPingOperator.Installer\WebPingOperator.Installer.csproj", "{DFCDEACF-E27B-412F-8636-5DA7A3D38FBC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebPingOperator.Model", ".\WebPingOperator.Model\WebPingOperator.Model.csproj", "{085364F0-8DB7-456B-AA2C-1594EB6329EF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebPingOperator.WebPingerArchiveController", ".\WebPingOperator.WebPingerArchiveController\WebPingOperator.WebPingerArchiveController.csproj", "{AAEE6551-29F4-427B-BED1-198DE14653D2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{27D653BB-04EA-470B-BE5C-A25D78D9DF8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27D653BB-04EA-470B-BE5C-A25D78D9DF8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27D653BB-04EA-470B-BE5C-A25D78D9DF8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27D653BB-04EA-470B-BE5C-A25D78D9DF8A}.Release|Any CPU.Build.0 = Release|Any CPU
{DFCDEACF-E27B-412F-8636-5DA7A3D38FBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DFCDEACF-E27B-412F-8636-5DA7A3D38FBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFCDEACF-E27B-412F-8636-5DA7A3D38FBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFCDEACF-E27B-412F-8636-5DA7A3D38FBC}.Release|Any CPU.Build.0 = Release|Any CPU
{085364F0-8DB7-456B-AA2C-1594EB6329EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{085364F0-8DB7-456B-AA2C-1594EB6329EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{085364F0-8DB7-456B-AA2C-1594EB6329EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{085364F0-8DB7-456B-AA2C-1594EB6329EF}.Release|Any CPU.Build.0 = Release|Any CPU
{AAEE6551-29F4-427B-BED1-198DE14653D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AAEE6551-29F4-427B-BED1-198DE14653D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAEE6551-29F4-427B-BED1-198DE14653D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAEE6551-29F4-427B-BED1-198DE14653D2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6F0A108E-19CF-43E6-911D-5E11AAD9A0E3}
EndGlobalSection
EndGlobal