├── Kncs.Webhook ├── .dockerignore ├── NotEmptyEnumerable.cs ├── appsettings.json ├── appsettings.Development.json ├── manifests │ ├── test-pods │ │ ├── injectee.yaml │ │ └── validatee.yaml │ ├── Dockerfile │ └── deploy.yaml ├── Kncs.Webhook.csproj ├── AdminReviewModels │ ├── generate-model.sh │ ├── admission.swagger.json │ └── AdmissionReview.cs ├── Program.cs ├── WebhookValidate.cs └── WebhookMutate.cs ├── Kncs.CrdController ├── manifests │ ├── runner │ │ ├── Dockerfile │ │ └── run.sh │ ├── samples │ │ ├── console.yaml │ │ └── web.yaml │ └── crd.yaml ├── appsettings.Development.json ├── appsettings.json ├── Kncs.CrdController.csproj ├── OperatorSDK │ ├── IOperatiornHandler.cs │ ├── BaseCrd.cs │ └── Controller.cs ├── Program.cs └── Crd │ ├── Models.cs │ └── CSharpAppOperator.cs ├── Kncs.ClientCs ├── manifests │ ├── rbac-no-permission.yaml │ ├── copy-exe.sh │ ├── test-pod.yaml │ └── rbac-good-permission.yaml ├── Kncs.ClientCs.csproj └── Program.cs ├── Kncs.CmdExecuter ├── Manifests │ └── pod.yaml ├── Kncs.CmdExecuter.csproj ├── Program.cs └── ProcessAsyncHelper.cs ├── kncs.sln.DotSettings ├── README.md ├── .gitignore └── kncs.sln /Kncs.Webhook/.dockerignore: -------------------------------------------------------------------------------- 1 | obj 2 | bin 3 | manifests -------------------------------------------------------------------------------- /Kncs.CrdController/manifests/runner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0 2 | COPY run.sh /run.sh 3 | ENTRYPOINT ["/bin/bash", "-f", "/run.sh"] 4 | 5 | -------------------------------------------------------------------------------- /Kncs.CrdController/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Kncs.Webhook/NotEmptyEnumerable.cs: -------------------------------------------------------------------------------- 1 | static class NotEmptyEnumerable 2 | { 3 | public static IEnumerable NotEmpty(this IEnumerable? items) 4 | { 5 | return items ?? Array.Empty(); 6 | } 7 | } -------------------------------------------------------------------------------- /Kncs.Webhook/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Kncs.CrdController/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Kncs.Webhook/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Kncs.ClientCs/manifests/rbac-no-permission.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: pod-operator-role 6 | namespace: default 7 | rules: 8 | - apiGroups: [ "" ] 9 | resources: [ "pods", "pods/exec" ] 10 | verbs: [ "create" ] -------------------------------------------------------------------------------- /Kncs.CrdController/manifests/runner/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SRC=/etc/cs-source/Program.cs 4 | if [ ! -f "$SRC" ]; then 5 | echo "No C# source file detected." 6 | echo 1 7 | fi 8 | 9 | WORK_DIR=/tmp/Proj$RANDOM 10 | mkdir -p $WORK_DIR 11 | 12 | cd $WORK_DIR 13 | dotnet new web 14 | cp $SRC $WORK_DIR/ 15 | dotnet run 16 | -------------------------------------------------------------------------------- /Kncs.CrdController/manifests/samples/console.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: k8s.jijiechen.com/v1alpha1 3 | kind: CSharpApp 4 | metadata: 5 | name: hello-console 6 | namespace: default 7 | spec: 8 | code: | 9 | Console.WriteLine("Hello world from CSharp App"); 10 | Thread.Sleep(Timeout.Infinite); 11 | replicas: 2 12 | 13 | -------------------------------------------------------------------------------- /Kncs.CmdExecuter/Manifests/pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: test 6 | name: '' 7 | spec: 8 | containers: 9 | - args: 10 | - infinity 11 | command: 12 | - sleep 13 | image: centos:7 14 | imagePullPolicy: IfNotPresent 15 | name: tester 16 | 17 | -------------------------------------------------------------------------------- /Kncs.ClientCs/manifests/copy-exe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # publish project: 4 | # dotnet publish -r linux-x64 -p:PublishSingleFile=true 5 | 6 | # Copy to test-pod: 7 | kubectl cp ~/Projects/kncs/Kncs.ClientCs/bin/Debug/net7.0/linux-x64/publish/Kncs.ClientCs default/test-pod:/tmp/ -c dotnet-helper 8 | kubectl exec -it test-pod -c dotnet-helper -- /bin/bash 9 | 10 | -------------------------------------------------------------------------------- /Kncs.ClientCs/manifests/test-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: test 6 | name: test-pod 7 | spec: 8 | serviceAccountName: pod-operator 9 | containers: 10 | - args: 11 | - infinity 12 | command: 13 | - sleep 14 | image: centos:7 15 | imagePullPolicy: IfNotPresent 16 | name: tester 17 | 18 | -------------------------------------------------------------------------------- /Kncs.Webhook/manifests/test-pods/injectee.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | labels: 5 | app: test 6 | name: webhook-tester-mutating 7 | namespace: default 8 | spec: 9 | containers: 10 | - args: 11 | - infinity 12 | command: 13 | - sleep 14 | image: centos:7 15 | imagePullPolicy: IfNotPresent 16 | name: tester 17 | 18 | -------------------------------------------------------------------------------- /Kncs.CrdController/manifests/samples/web.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: k8s.jijiechen.com/v1alpha1 3 | kind: CSharpApp 4 | metadata: 5 | name: hello-web 6 | namespace: default 7 | spec: 8 | code: | 9 | var app = WebApplication.Create(args); 10 | app.MapGet("/", () => "Hello World!"); 11 | app.Run("http://*:80"); 12 | replicas: 2 13 | service: 14 | type: LoadBalancer 15 | port: 80 16 | -------------------------------------------------------------------------------- /kncs.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | False -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 使用 CSharp 开发 Kubernetes 原生基础设施 3 | ====== 4 | 5 | 6 | 包含如下示例: 7 | 8 | 1. kncs.CmdExecuter 展示如何在 C# 中读取、生成并编辑 YAML,最终传入 kubectl 命令行,动态地在 Kubernetes 集群中创建 Pod 9 | 1. kncs.ClientCs 展示如何以 C# 编程的方式与 Kubernetes 集群交互,读取和监视 Pod 资源的状态 10 | 1. kncs.Webhook 展示如何用 C# 实现 Kubernetes Webhook,实现对 Pod 镜像的检查,并实现类似 Istio 中的容器自动注入的功能 11 | 1. kncs.CrdController 展示如何实现自定义资源类型 CRD:为 Kubernetes 集群安装新的 CSharpApp 资源类型 12 | 13 | -------------------------------------------------------------------------------- /Kncs.CrdController/Kncs.CrdController.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Kncs.Webhook/manifests/test-pods/validatee.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | annotations: 5 | k8s.jijiechen.com/inject-dotnet-helper: 'false' 6 | labels: 7 | app: test 8 | name: webhook-tester-validating 9 | namespace: default 10 | spec: 11 | containers: 12 | - args: 13 | - infinity 14 | command: 15 | - sleep 16 | image: abcd.com/centos:7 17 | imagePullPolicy: IfNotPresent 18 | name: tester 19 | 20 | -------------------------------------------------------------------------------- /Kncs.ClientCs/Kncs.ClientCs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Kncs.CrdController/OperatorSDK/IOperatiornHandler.cs: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/ContainerSolutions/dotnet-operator-sdk 2 | 3 | using k8s; 4 | 5 | namespace Kncs.CrdController.OperatorSDK; 6 | 7 | public interface IOperationHandler where T : BaseCRD 8 | { 9 | Task OnAdded(Kubernetes k8s, T crd); 10 | 11 | Task OnDeleted(Kubernetes k8s, T crd); 12 | 13 | Task OnUpdated(Kubernetes k8s, T crd); 14 | 15 | Task OnBookmarked(Kubernetes k8s, T crd); 16 | 17 | Task OnError(Kubernetes k8s, T crd); 18 | 19 | Task CheckCurrentState(Kubernetes k8s); 20 | } -------------------------------------------------------------------------------- /Kncs.Webhook/Kncs.Webhook.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | *.sln.ide/ 6 | _ReSharper.*/ 7 | .idea/ 8 | packages/ 9 | artifacts/ 10 | PublishProfiles/ 11 | .vs/ 12 | *.user 13 | *.suo 14 | *.cache 15 | *.docstates 16 | _ReSharper.* 17 | nuget.exe 18 | *net45.csproj 19 | *net451.csproj 20 | *k10.csproj 21 | *.psess 22 | *.vsp 23 | *.pidb 24 | *.userprefs 25 | *DS_Store 26 | *.ncrunchsolution 27 | *.*sdf 28 | *.ipch 29 | *.swp 30 | *~ 31 | .build/ 32 | .testPublish/ 33 | launchSettings.json 34 | BenchmarkDotNet.Artifacts/ 35 | BDN.Generated/ 36 | binaries/ 37 | global.json 38 | .vscode/ 39 | *.binlog 40 | build/feed 41 | .dotnet/ 42 | *.log.txt 43 | -------------------------------------------------------------------------------- /Kncs.CmdExecuter/Kncs.CmdExecuter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Kncs.ClientCs/manifests/rbac-good-permission.yaml: -------------------------------------------------------------------------------- 1 | kind: ServiceAccount 2 | apiVersion: v1 3 | metadata: 4 | name: pod-operator 5 | namespace: default 6 | 7 | --- 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: Role 10 | metadata: 11 | name: pod-operator-role 12 | namespace: default 13 | rules: 14 | - apiGroups: [ "" ] 15 | resources: [ "pods", "pods/exec" ] 16 | verbs: [ "*" ] 17 | 18 | --- 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | kind: RoleBinding 21 | metadata: 22 | name: pod-operator-role 23 | namespace: default 24 | roleRef: 25 | apiGroup: rbac.authorization.k8s.io 26 | kind: Role 27 | name: pod-operator-role 28 | subjects: 29 | - kind: ServiceAccount 30 | name: pod-operator 31 | namespace: default -------------------------------------------------------------------------------- /Kncs.Webhook/manifests/Dockerfile: -------------------------------------------------------------------------------- 1 | # cd .. 2 | # docker build -t jijiechen-docker.pkg.coding.net/sharpcr/kncs/webhook:$(date +"%Y%m%d")-$RANDOM -f ./manifests/Dockerfile . 3 | FROM mcr.microsoft.com/dotnet/sdk:7.0.100-preview.4-alpine3.15-amd64 as Builder 4 | WORKDIR /src/ 5 | # copy csproj only to reuse nuget cache when csproj is not changing 6 | COPY *.csproj /src/ 7 | RUN dotnet restore 8 | 9 | COPY . /src/ 10 | RUN mkdir /app && dotnet publish -c Release -p:PublishSingleFile=true -p:PublishTrimmed=true -r linux-musl-x64 --self-contained -o /app 11 | 12 | 13 | 14 | 15 | FROM alpine:3.15 16 | # Install the dependencies according to https://docs.microsoft.com/en-us/dotnet/core/install/linux-alpine#dependencies 17 | RUN apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib 18 | 19 | WORKDIR /app 20 | COPY --from=Builder /app/* ./ 21 | 22 | ENTRYPOINT ["/app/Kncs.Webhook"] 23 | 24 | -------------------------------------------------------------------------------- /Kncs.Webhook/AdminReviewModels/generate-model.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # cmd_nswag=$(command -v nswag || true) 4 | # if [ "$cmd_nswag" == "" ]; then 5 | # npm install -g nswag 6 | # nswag version /runtime:NetCore31 7 | # fi 8 | 9 | # fetch swagger scheme from https://gist.github.com/bergeron/70ca86cf31762e16f18b2be3c549a074 10 | if [ ! -f "./admission.swagger.json" ]; then 11 | curl --fail -o admission.swagger.json https://gist.githubusercontent.com/bergeron/70ca86cf31762e16f18b2be3c549a074/raw/77c67214eff1c9edf7b133947c0d0ff557dcdc6f/k8s.io.api.admission.v1.swagger.json 12 | fi 13 | 14 | nswag openapi2csclient /input:admission.swagger.json /classname:AdmissionReview /namespace:Kncs.Webhook /output:AdmissionReview.cs 15 | sed -i 's/public RawExtension/public System.Text.Json.JsonElement/g' ./AdmissionReview.cs 16 | sed -i 's/\[Newtonsoft.Json.JsonProperty/[System.Text.Json.Serialization.JsonPropertyName/g' ./AdmissionReview.cs 17 | sed -i 's/,\ Required.*/)]/g' ./AdmissionReview.cs 18 | -------------------------------------------------------------------------------- /Kncs.Webhook/Program.cs: -------------------------------------------------------------------------------- 1 | using Kncs.Webhook; 2 | using Microsoft.AspNetCore.HttpLogging; 3 | 4 | var debugEnabled = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DEBUG")); 5 | var builder = WebApplication.CreateBuilder(args); 6 | if (debugEnabled) 7 | { 8 | builder.Services.AddHttpLogging(options => 9 | { 10 | options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders | 11 | HttpLoggingFields.ResponsePropertiesAndHeaders | 12 | HttpLoggingFields.RequestBody | HttpLoggingFields.ResponseBody; 13 | }); 14 | } 15 | 16 | 17 | var app = builder.Build(); 18 | if (debugEnabled) 19 | { 20 | app.UseHttpLogging(); 21 | } 22 | 23 | app.MapGet("/", () => "Hello .NET Kubernetes Webhook!"); 24 | app.MapPost("/validate", (AdmissionReview review) => Task.FromResult(CheckImage(review))); 25 | app.MapPost("/mutate", (AdmissionReview review) => Task.FromResult(InjectDotnetHelper(review))); 26 | 27 | app.Run(); 28 | -------------------------------------------------------------------------------- /Kncs.CrdController/Program.cs: -------------------------------------------------------------------------------- 1 | using Kncs.CrdController.OperatorSDK; 2 | using Kncs.CrdController.Crd; 3 | using NLog.Fluent; 4 | 5 | namespace Kncs.CrdController; 6 | 7 | public class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | try 12 | { 13 | string k8sNamespace = "default"; 14 | if (args.Length > 1) 15 | k8sNamespace = args[0]; 16 | 17 | Controller.ConfigLogger(); 18 | 19 | Log.Info($"=== STARTING CSharpApp for {k8sNamespace} ==="); 20 | 21 | var controller = new Controller(new CSharpApp(), new CSharpAppOperator(k8sNamespace), k8sNamespace); 22 | Task reconciliation = controller.SatrtAsync(); 23 | 24 | reconciliation.ConfigureAwait(false).GetAwaiter().GetResult(); 25 | } 26 | catch (Exception ex) 27 | { 28 | Log.Fatal(ex.Message + ex.StackTrace); 29 | throw; 30 | } 31 | finally 32 | { 33 | Log.Warn($"=== TERMINATING ==="); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Kncs.CrdController/manifests/crd.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: csharpapps.k8s.jijiechen.com 5 | spec: 6 | group: k8s.jijiechen.com 7 | versions: 8 | - name: v1alpha1 9 | served: true 10 | storage: true 11 | schema: 12 | openAPIV3Schema: 13 | type: object 14 | description: "An application running csharp source" 15 | properties: 16 | spec: 17 | type: object 18 | required: 19 | - code 20 | properties: 21 | code: 22 | type: string 23 | replicas: 24 | type: integer 25 | format: int32 26 | service: 27 | type: object 28 | description: "Service properties when the application provides network services" 29 | properties: 30 | port: 31 | type: integer 32 | format: int32 33 | type: 34 | enum: 35 | - LoadBalancer 36 | - ClusterIP 37 | - NodePort 38 | type: string 39 | required: 40 | - port 41 | 42 | scope: Namespaced 43 | names: 44 | plural: csharpapps 45 | singular: csharpapp 46 | kind: CSharpApp 47 | shortNames: 48 | - csa 49 | -------------------------------------------------------------------------------- /Kncs.CrdController/OperatorSDK/BaseCrd.cs: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/ContainerSolutions/dotnet-operator-sdk 2 | // Json properties added 3 | 4 | using System.Text.Json.Serialization; 5 | using k8s; 6 | using k8s.Models; 7 | using Newtonsoft.Json; 8 | 9 | namespace Kncs.CrdController.OperatorSDK; 10 | 11 | public abstract class BaseCRD : IMetadata 12 | { 13 | protected BaseCRD(string group, string version, string plural, string singular, int reconInterval = 5) 14 | { 15 | Group = group; 16 | Version = version ?? throw new ArgumentNullException(nameof(version)); 17 | Plural = plural; 18 | Singular = singular; 19 | ReconciliationCheckInterval = reconInterval; 20 | } 21 | 22 | public int ReconciliationCheckInterval { get; protected set; } 23 | public string Group { get; protected set; } 24 | public string Version { get; protected set; } 25 | public string Plural { get; protected set; } 26 | public string Singular { get; protected set; } 27 | public string StatusAnnotationName { get => string.Format($"{Group}/{Singular}-status"); } 28 | 29 | [JsonProperty(PropertyName="status")] 30 | public string? Status => Metadata.Annotations == null ? null : 31 | (Metadata.Annotations.ContainsKey(StatusAnnotationName) ? Metadata.Annotations[StatusAnnotationName] : null); 32 | [JsonProperty(PropertyName="apiVersion")] 33 | public string ApiVersion { get; set; } = null!; 34 | 35 | [JsonProperty(PropertyName="kind")] 36 | public string Kind { get; set; } = null!; 37 | 38 | [JsonProperty(PropertyName="metadata")] 39 | public V1ObjectMeta Metadata { get; set; } = null!; 40 | } -------------------------------------------------------------------------------- /kncs.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kncs.ClientCs", "Kncs.ClientCs\Kncs.ClientCs.csproj", "{9EB48734-92BA-4D1C-9C82-01ED35397E45}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kncs.CmdExecuter", "Kncs.CmdExecuter\Kncs.CmdExecuter.csproj", "{E274E64B-9FF4-4A1B-B9D1-4DEFE123251D}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kncs.CrdController", "Kncs.CrdController\Kncs.CrdController.csproj", "{F0ADAC7D-4661-45FA-8F4B-A654DFB8A7D3}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kncs.Webhook", "Kncs.Webhook\Kncs.Webhook.csproj", "{6F593CF7-7B58-46BA-BBD4-EC52E91A46EE}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {9EB48734-92BA-4D1C-9C82-01ED35397E45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {9EB48734-92BA-4D1C-9C82-01ED35397E45}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {9EB48734-92BA-4D1C-9C82-01ED35397E45}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {9EB48734-92BA-4D1C-9C82-01ED35397E45}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {E274E64B-9FF4-4A1B-B9D1-4DEFE123251D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {E274E64B-9FF4-4A1B-B9D1-4DEFE123251D}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {E274E64B-9FF4-4A1B-B9D1-4DEFE123251D}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {E274E64B-9FF4-4A1B-B9D1-4DEFE123251D}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {F0ADAC7D-4661-45FA-8F4B-A654DFB8A7D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {F0ADAC7D-4661-45FA-8F4B-A654DFB8A7D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {F0ADAC7D-4661-45FA-8F4B-A654DFB8A7D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {F0ADAC7D-4661-45FA-8F4B-A654DFB8A7D3}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {6F593CF7-7B58-46BA-BBD4-EC52E91A46EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {6F593CF7-7B58-46BA-BBD4-EC52E91A46EE}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {6F593CF7-7B58-46BA-BBD4-EC52E91A46EE}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {6F593CF7-7B58-46BA-BBD4-EC52E91A46EE}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /Kncs.ClientCs/Program.cs: -------------------------------------------------------------------------------- 1 | using k8s; 2 | using k8s.Models; 3 | using Nito.AsyncEx; 4 | 5 | namespace Kncs.ClientCs; 6 | 7 | public class Program 8 | { 9 | static async Task Main() 10 | { 11 | // 使用凭据构建一个客户端 12 | var k8sConfig = KubernetesClientConfiguration.BuildDefaultConfig(); 13 | var kubeClient = new Kubernetes(k8sConfig); 14 | 15 | // 找出现有的 Pod 16 | var pods = await kubeClient.ListNamespacedPodAsync("default", 17 | null, null, null, "app=test"); 18 | foreach (var pod in pods.Items) 19 | { 20 | Console.WriteLine($"现有的 test Pod: {pod.Metadata.Name}"); 21 | } 22 | 23 | if (pods.Items.Count < 1) 24 | { 25 | return; 26 | } 27 | 28 | // 在现有的 Pod 中执行命令 29 | foreach (var pod in pods.Items) 30 | { 31 | var execResult = await ExecInPod(kubeClient, pod, "hostname -I"); 32 | Console.WriteLine($"Pod {pod.Metadata.Name} 执行结果 {execResult}"); 33 | } 34 | 35 | // watch 新的 Pod 36 | var existingPods = pods.Items.Select(p => p.Metadata.Name).ToHashSet(); 37 | var listTask = 38 | await kubeClient.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).ConfigureAwait(false); 39 | var connectionClosed = new AsyncManualResetEvent(); 40 | 41 | listTask.Watch( 42 | (type, item) => 43 | { 44 | if (type == WatchEventType.Added && !string.IsNullOrEmpty(item?.Metadata.Name) && 45 | existingPods.Contains(item.Metadata.Name)) 46 | { 47 | return; 48 | } 49 | 50 | Console.WriteLine($"监视到事件 '{type}',相关 Pod: {item!.Metadata.Name}"); 51 | }, 52 | error => 53 | { 54 | Console.WriteLine($"监视到错误 '{error.GetType().FullName}'"); 55 | }, 56 | connectionClosed.Set); 57 | 58 | Console.WriteLine("等待新的 Pod 事件..."); 59 | await connectionClosed.WaitAsync(); 60 | } 61 | 62 | private static async Task ExecInPod(IKubernetes client, V1Pod pod, string command) 63 | { 64 | var webSocket = await client.WebSocketNamespacedPodExecAsync(pod.Metadata.Name, "default", command.Split(" "), 65 | pod.Spec.Containers[0].Name).ConfigureAwait(false); 66 | 67 | using var demux = new StreamDemuxer(webSocket); 68 | demux.Start(); 69 | 70 | var buff = new byte[4096]; 71 | var stream = demux.GetStream(1, 1); 72 | var read = stream.Read(buff, 0, 4096); 73 | return read > 0 ? System.Text.Encoding.Default.GetString(buff, 0, read) : string.Empty; 74 | } 75 | 76 | 77 | 78 | 79 | } -------------------------------------------------------------------------------- /Kncs.CrdController/Crd/Models.cs: -------------------------------------------------------------------------------- 1 | using Kncs.CrdController.OperatorSDK; 2 | using k8s; 3 | using k8s.Models; 4 | using Newtonsoft.Json; 5 | 6 | namespace Kncs.CrdController.Crd; 7 | 8 | public class CSharpAppSpec 9 | { 10 | [JsonProperty(PropertyName="code")] 11 | public string? Code { get; set; } 12 | 13 | [JsonProperty(PropertyName="replicas")] 14 | public byte Replicas { get; set; } 15 | [JsonProperty(PropertyName="service")] 16 | public CSharpAppService? Service { get; set; } 17 | } 18 | 19 | public class CSharpAppService 20 | { 21 | [JsonProperty(PropertyName="port")] 22 | public short Port { get; set; } 23 | [JsonProperty(PropertyName="type")] 24 | public string? Type { get; set; } 25 | 26 | public override bool Equals(object? obj) 27 | { 28 | var item = obj as CSharpAppService; 29 | 30 | if (item == null) 31 | { 32 | return false; 33 | } 34 | 35 | return this.Port == item.Port && (bool)(this.Type!.Equals(item.Type)); 36 | } 37 | 38 | // ReSharper disable NonReadonlyMemberInGetHashCode 39 | public override int GetHashCode() 40 | { 41 | if (this.Type == null) 42 | { 43 | return this.Port.GetHashCode() ^ "null".GetHashCode(); 44 | } 45 | 46 | 47 | return this.Port.GetHashCode() ^ Type.GetHashCode(); 48 | } 49 | } 50 | 51 | public class LastApplyConfiguration 52 | { 53 | // ReSharper disable once InconsistentNaming 54 | public string? CodeHash { get; set; } 55 | public byte Replicas { get; set; } 56 | public CSharpAppService? Service { get; set; } 57 | } 58 | 59 | public class CSharpApp: BaseCRD 60 | { 61 | public const string SchemaGroup = "k8s.jijiechen.com"; 62 | public const string SchemaKindSingular = "csharpapp"; 63 | public const string SchemaKindPlural = "csharpapps"; 64 | public const string SchemaVersion = "v1alpha1"; 65 | 66 | 67 | public CSharpApp() : base(SchemaGroup, SchemaVersion, SchemaKindPlural, SchemaKindSingular) { } 68 | 69 | [JsonProperty(PropertyName="spec")] 70 | public CSharpAppSpec? Spec { get; set; } 71 | 72 | public override bool Equals(object? obj) 73 | { 74 | if (obj == null) 75 | return false; 76 | 77 | return ToString().Equals(obj.ToString()); 78 | } 79 | 80 | public override int GetHashCode() 81 | { 82 | return ToString().GetHashCode(); 83 | } 84 | 85 | public override string ToString() 86 | { 87 | return Spec?.ToString() ?? string.Empty; 88 | } 89 | 90 | } 91 | 92 | public class CSharpAppList : IKubernetesObject, IItems 93 | { 94 | [JsonProperty(PropertyName="apiVersion")] 95 | public string? ApiVersion { get; set; } 96 | 97 | [JsonProperty(PropertyName="items")] 98 | public IList? Items { get; set; } 99 | 100 | [JsonProperty(PropertyName="kind")] 101 | public string? Kind { get; set; } 102 | 103 | [JsonProperty(PropertyName="metadata")] 104 | #pragma warning disable CS8618 105 | public V1ListMeta Metadata { get; set; } 106 | #pragma warning restore CS8618 107 | } -------------------------------------------------------------------------------- /Kncs.CmdExecuter/Program.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Diagnostics; 3 | using System.Dynamic; 4 | using System.Reflection; 5 | using k8s; 6 | using k8s.Models; 7 | using YamlDotNet.Serialization; 8 | using YamlDotNet.Serialization.NamingConventions; 9 | 10 | namespace Kncs.CmdExecuter; 11 | 12 | public class Program 13 | { 14 | static void Main(string[] args) 15 | { 16 | Console.WriteLine("已安装的 kubectl 版本: "); 17 | Console.WriteLine(ExecuteKubectl("version", null)); 18 | 19 | var random = new Random().Next(10000, 99999); 20 | var podName = $"testpod-{random}"; 21 | 22 | var podYaml = GetPodYaml(podName); 23 | if (args.Length > 0) 24 | { 25 | podName = $"csharp-{podName}"; 26 | podYaml = ComposePodAsYaml(podName); 27 | } 28 | Console.WriteLine($"准备创建 Pod {podName}:"); 29 | Console.WriteLine(podYaml); 30 | 31 | var createOutput = ExecuteKubectl("-n default create -f -", podYaml); 32 | Console.WriteLine("创建 Pod 的结果:"); 33 | Console.WriteLine(createOutput); 34 | 35 | var getOutput = ExecuteKubectl($"-n default get pods {podName}", null); 36 | Console.WriteLine("获取 Pod 的结果:"); 37 | Console.WriteLine(getOutput); 38 | } 39 | 40 | static string ExecuteKubectl(string args, string? stdin) 41 | { 42 | var kubectl = new ProcessStartInfo("kubectl", args) 43 | { 44 | RedirectStandardOutput = true 45 | }; 46 | var kubectlTask = ProcessAsyncHelper.RunAsync(kubectl, stdin); 47 | kubectlTask.ConfigureAwait(false).GetAwaiter().GetResult(); 48 | return kubectlTask.Result.StdOut; 49 | } 50 | 51 | static string? GetPodYaml(string podName) 52 | { 53 | using var resStream = Assembly.GetExecutingAssembly() 54 | .GetManifestResourceStream("Kncs.CmdExecuter.Manifests.pod.yaml"); 55 | if (resStream == null) return string.Empty; 56 | 57 | using var reader = new StreamReader(resStream); 58 | var podYamlTemplate = reader.ReadToEnd(); 59 | 60 | var deserializer = new DeserializerBuilder() 61 | .WithNamingConvention(NullNamingConvention.Instance) 62 | .Build(); 63 | 64 | dynamic p = deserializer.Deserialize(podYamlTemplate); 65 | p.metadata["name"] = podName; 66 | 67 | var serializer = new SerializerBuilder() 68 | .WithNamingConvention(NullNamingConvention.Instance) 69 | .Build(); 70 | 71 | var writer = new StringWriter(); 72 | serializer.Serialize(writer, p); 73 | 74 | return writer.ToString(); 75 | } 76 | 77 | 78 | static string ComposePodAsYaml(string podName) 79 | { 80 | var pod = new V1Pod 81 | { 82 | ApiVersion = "v1", 83 | Kind = "Pod", 84 | Spec = new V1PodSpec(){Containers = new List()}, 85 | Metadata = new V1ObjectMeta(){Labels = new Dictionary()} 86 | }; 87 | 88 | pod.Metadata.Name = podName; 89 | pod.Metadata.Labels["app"] = "test"; 90 | 91 | var container = new V1Container("tester") 92 | { 93 | Args = new[] { "infinity" }, 94 | Command = new[] { "sleep" }, 95 | Image = "centos:7", 96 | ImagePullPolicy = "IfNotPresent" 97 | }; 98 | pod.Spec.Containers.Add(container); 99 | 100 | return KubernetesYaml.Serialize(pod); 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /Kncs.Webhook/WebhookValidate.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text.Json; 3 | using k8s.Models; 4 | using Kncs.Webhook; 5 | using JsonSerializerOptions = System.Text.Json.JsonSerializerOptions; 6 | 7 | partial class Program 8 | { 9 | private static string[]? _blockedRepos = null; 10 | 11 | static AdmissionReview CheckImage(AdmissionReview reviewRequest) 12 | { 13 | if (_blockedRepos == null) 14 | { 15 | _blockedRepos = (Environment.GetEnvironmentVariable("BLOCKED_REPOS") ?? "").Split(","); 16 | } 17 | 18 | var allowed = true; 19 | if (reviewRequest.Request.Kind.Kind != "Pod" || reviewRequest.Request.Operation != "CREATE") 20 | { 21 | return CreateReviewResponse(reviewRequest, allowed); 22 | } 23 | 24 | var pod = JsonSerializer.Deserialize(reviewRequest.Request.Object.GetRawText(), 25 | new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); 26 | if (pod == null) 27 | { 28 | return CreateReviewResponse(reviewRequest, allowed); 29 | } 30 | 31 | var podName = string.IsNullOrEmpty(reviewRequest.Request.Name) ? pod.Metadata.GenerateName + "?" : reviewRequest.Request.Name; 32 | var fullPodName = $"{reviewRequest.Request.Namespace}/{podName}"; 33 | var usedImages = new List() 34 | .Concat(pod.Spec.Containers.NotEmpty().Select(c => c.Image)) 35 | .Concat(pod.Spec.InitContainers.NotEmpty().Select(c => c.Image)) 36 | .Distinct() 37 | .ToList(); 38 | 39 | var blockedImages = new List(); 40 | var blockedCount = usedImages.Select(img => 41 | { 42 | var shouldBlock = ImageBlocked(img); 43 | if (shouldBlock) 44 | { 45 | blockedImages.Add(img); 46 | Console.WriteLine($"Pod {fullPodName} 被拦截,因为使用被禁止的镜像 {img}"); 47 | } 48 | 49 | return shouldBlock; 50 | }).Count(b => b); 51 | 52 | allowed = blockedCount == 0; 53 | var res = CreateReviewResponse(reviewRequest, allowed); 54 | if (!allowed) 55 | { 56 | var imageList = string.Join(",", blockedImages); 57 | res.Response.Status.Message = $"dotnet webhook: Pod should not use these images: {imageList}"; 58 | } 59 | 60 | return res; 61 | } 62 | 63 | static bool ImageBlocked(string imageLocation) 64 | { 65 | if (_blockedRepos!.Length == 1 && string.IsNullOrWhiteSpace(_blockedRepos[0])) 66 | { 67 | return false; 68 | } 69 | 70 | if (!imageLocation.Contains('/')) 71 | { 72 | imageLocation = "docker.io/" + imageLocation; 73 | } 74 | 75 | return _blockedRepos 76 | .Select(r => r.EndsWith('/') ? r : string.Concat(r, '/')) 77 | .Any(r => imageLocation.StartsWith(r)); 78 | } 79 | 80 | static AdmissionReview CreateReviewResponse(AdmissionReview originalRequest, bool allowed) 81 | { 82 | var res = new AdmissionResponse 83 | { 84 | Allowed = allowed, 85 | Status = new Status 86 | { 87 | Code = (int)HttpStatusCode.OK, 88 | Message = string.Empty 89 | } 90 | }; 91 | 92 | res.Uid = originalRequest.Request.Uid; 93 | return new AdmissionReview 94 | { 95 | ApiVersion = originalRequest.ApiVersion, 96 | Kind = originalRequest.Kind, 97 | Response = res 98 | }; 99 | } 100 | } -------------------------------------------------------------------------------- /Kncs.Webhook/WebhookMutate.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text; 3 | using System.Text.Json; 4 | using k8s.Models; 5 | using Kncs.Webhook; 6 | 7 | partial class Program 8 | { 9 | 10 | const string InjectAnnotationKeySetting = "k8s.jijiechen.com/inject-dotnet-helper"; 11 | 12 | static AdmissionReview InjectDotnetHelper(AdmissionReview reviewRequest) 13 | { 14 | var allowedResponse = CreateInjectResponse(reviewRequest, true); 15 | if (reviewRequest.Request.Kind.Kind != "Pod" || reviewRequest.Request.Operation != "CREATE") 16 | { 17 | return allowedResponse; 18 | } 19 | 20 | var podJson = reviewRequest.Request.Object.GetRawText(); 21 | var pod = JsonSerializer.Deserialize(podJson, 22 | new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); 23 | if (DisabledByAnnotation(pod!.Metadata.Annotations)) 24 | { 25 | return allowedResponse; 26 | } 27 | 28 | if (AlreadyInjected(pod)) 29 | { 30 | return allowedResponse; 31 | } 32 | 33 | var container = new V1Container("dotnet-helper") 34 | { 35 | Args = new[] { "infinity" }, 36 | Command = new[] { "sleep" }, 37 | Image = "mcr.microsoft.com/dotnet/runtime:6.0", 38 | ImagePullPolicy = "IfNotPresent" 39 | }; 40 | 41 | var podName = string.IsNullOrEmpty(reviewRequest.Request.Name) 42 | ? pod.Metadata.GenerateName + "?" 43 | : reviewRequest.Request.Name; 44 | var fullPodName = $"{reviewRequest.Request.Namespace}/{podName}"; 45 | Console.WriteLine($"正在向此 Pod 中注入 dotnet helper:{fullPodName}"); 46 | var patch = new 47 | { 48 | op = "add", 49 | path = "/spec/containers/-", 50 | value = container 51 | }; 52 | var patches = new[] { patch }; 53 | var patchResponse = new AdmissionResponse() 54 | { 55 | Allowed = true, 56 | PatchType = "JSONPatch", 57 | Patch = Encoding.Default.GetBytes(JsonSerializer.Serialize(patches)) 58 | }; 59 | var reviewResponse = CreateInjectResponse(reviewRequest, true); 60 | reviewResponse.Response = patchResponse; 61 | return reviewResponse; 62 | } 63 | 64 | private static bool AlreadyInjected(V1Pod pod) 65 | { 66 | return pod.Spec.Containers.Any(c => c.Name == "dotnet-helper"); 67 | } 68 | 69 | private static bool DisabledByAnnotation(IDictionary? annotations) 70 | { 71 | if (annotations == null) 72 | { 73 | return false; 74 | } 75 | 76 | var falseValues = new[] { "no", "false", "0" }; 77 | 78 | return annotations.TryGetValue(InjectAnnotationKeySetting, out var annotation) && 79 | falseValues.Contains(annotation.ToLower()); 80 | } 81 | 82 | static AdmissionReview CreateInjectResponse(AdmissionReview originalRequest, bool allowed) 83 | { 84 | var res = new AdmissionResponse 85 | { 86 | Allowed = allowed, 87 | Status = new Status 88 | { 89 | Code = (int)HttpStatusCode.OK, 90 | Message = string.Empty 91 | } 92 | }; 93 | 94 | res.Uid = originalRequest.Request.Uid; 95 | return new AdmissionReview 96 | { 97 | ApiVersion = originalRequest.ApiVersion, 98 | Kind = originalRequest.Kind, 99 | Response = res 100 | }; 101 | } 102 | } -------------------------------------------------------------------------------- /Kncs.CmdExecuter/ProcessAsyncHelper.cs: -------------------------------------------------------------------------------- 1 | // code taken from https://gist.github.com/Indigo744/b5f3bd50df4b179651c876416bf70d0a 2 | 3 | using System.Diagnostics; 4 | using System.Text; 5 | 6 | namespace Kncs.CmdExecuter; 7 | 8 | 9 | /// 10 | /// Process helper with asynchronous interface 11 | /// - Based on https://gist.github.com/georg-jung/3a8703946075d56423e418ea76212745 12 | /// - And on https://stackoverflow.com/questions/470256/process-waitforexit-asynchronously 13 | /// 14 | public static class ProcessAsyncHelper 15 | { 16 | /// 17 | /// Run a process asynchronously 18 | /// To capture STDOUT, set StartInfo.RedirectStandardOutput to TRUE 19 | /// To capture STDERR, set StartInfo.RedirectStandardError to TRUE 20 | /// 21 | /// ProcessStartInfo object 22 | /// The timeout in milliseconds (null for no timeout) 23 | /// Result object 24 | public static async Task RunAsync(ProcessStartInfo startInfo, string? stdIn = null, int? timeoutMs = null) 25 | { 26 | Result result = new Result(); 27 | 28 | if (!string.IsNullOrWhiteSpace(stdIn)) 29 | { 30 | startInfo.RedirectStandardInput = true; 31 | } 32 | 33 | using (var process = new Process() { StartInfo = startInfo, EnableRaisingEvents = true }) 34 | { 35 | // List of tasks to wait for a whole process exit 36 | List processTasks = new List(); 37 | 38 | // === EXITED Event handling === 39 | var processExitEvent = new TaskCompletionSource(); 40 | process.Exited += (sender, args) => 41 | { 42 | processExitEvent.TrySetResult(true); 43 | }; 44 | processTasks.Add(processExitEvent.Task); 45 | 46 | // === STDOUT handling === 47 | var stdOutBuilder = new StringBuilder(); 48 | if (process.StartInfo.RedirectStandardOutput) 49 | { 50 | var stdOutCloseEvent = new TaskCompletionSource(); 51 | 52 | process.OutputDataReceived += (s, e) => 53 | { 54 | if (e.Data == null) 55 | { 56 | stdOutCloseEvent.TrySetResult(true); 57 | } 58 | else 59 | { 60 | stdOutBuilder.AppendLine(e.Data); 61 | } 62 | }; 63 | 64 | processTasks.Add(stdOutCloseEvent.Task); 65 | } 66 | else 67 | { 68 | // STDOUT is not redirected, so we won't look for it 69 | } 70 | 71 | // === STDERR handling === 72 | var stdErrBuilder = new StringBuilder(); 73 | if (process.StartInfo.RedirectStandardError) 74 | { 75 | var stdErrCloseEvent = new TaskCompletionSource(); 76 | 77 | process.ErrorDataReceived += (s, e) => 78 | { 79 | if (e.Data == null) 80 | { 81 | stdErrCloseEvent.TrySetResult(true); 82 | } 83 | else 84 | { 85 | stdErrBuilder.AppendLine(e.Data); 86 | } 87 | }; 88 | 89 | processTasks.Add(stdErrCloseEvent.Task); 90 | } 91 | else 92 | { 93 | // STDERR is not redirected, so we won't look for it 94 | } 95 | 96 | // === START OF PROCESS === 97 | if (!process.Start()) 98 | { 99 | result.ExitCode = process.ExitCode; 100 | return result; 101 | } 102 | 103 | 104 | // Read StdIn if provided 105 | if (process.StartInfo.RedirectStandardInput) 106 | { 107 | using (var writer = process.StandardInput) 108 | { 109 | writer.Write(stdIn); 110 | } 111 | } 112 | 113 | // Reads the output stream first as needed and then waits because deadlocks are possible 114 | if (process.StartInfo.RedirectStandardOutput) 115 | { 116 | process.BeginOutputReadLine(); 117 | } 118 | else 119 | { 120 | // No STDOUT 121 | } 122 | 123 | if (process.StartInfo.RedirectStandardError) 124 | { 125 | process.BeginErrorReadLine(); 126 | } 127 | else 128 | { 129 | // No STDERR 130 | } 131 | 132 | // === ASYNC WAIT OF PROCESS === 133 | 134 | // Process completion = exit AND stdout (if defined) AND stderr (if defined) 135 | Task processCompletionTask = Task.WhenAll(processTasks); 136 | 137 | // Task to wait for exit OR timeout (if defined) 138 | Task awaitingTask = timeoutMs.HasValue 139 | ? Task.WhenAny(Task.Delay(timeoutMs.Value), processCompletionTask) 140 | : Task.WhenAny(processCompletionTask); 141 | 142 | // Let's now wait for something to end... 143 | if ((await awaitingTask.ConfigureAwait(false)) == processCompletionTask) 144 | { 145 | // -> Process exited cleanly 146 | result.ExitCode = process.ExitCode; 147 | } 148 | else 149 | { 150 | // -> Timeout, let's kill the process 151 | try 152 | { 153 | process.Kill(); 154 | } 155 | catch 156 | { 157 | // ignored 158 | } 159 | } 160 | 161 | // Read stdout/stderr 162 | result.StdOut = stdOutBuilder.ToString(); 163 | result.StdErr = stdErrBuilder.ToString(); 164 | } 165 | 166 | return result; 167 | } 168 | 169 | /// 170 | /// Run process result 171 | /// 172 | public class Result 173 | { 174 | /// 175 | /// Exit code 176 | /// If NULL, process exited due to timeout 177 | /// 178 | public int? ExitCode { get; set; } = null; 179 | 180 | /// 181 | /// Standard error stream 182 | /// 183 | public string StdErr { get; set; } = ""; 184 | 185 | /// 186 | /// Standard output stream 187 | /// 188 | public string StdOut { get; set; } = ""; 189 | } 190 | } -------------------------------------------------------------------------------- /Kncs.CrdController/OperatorSDK/Controller.cs: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/ContainerSolutions/dotnet-operator-sdk 2 | 3 | using k8s; 4 | using k8s.Models; 5 | using Microsoft.Rest; 6 | using NLog; 7 | using NLog.Config; 8 | using NLog.Targets; 9 | using LogLevel = NLog.LogLevel; 10 | 11 | namespace Kncs.CrdController.OperatorSDK; 12 | 13 | public class Controller where T : BaseCRD 14 | { 15 | private static readonly Logger Log = LogManager.GetCurrentClassLogger(); 16 | public Kubernetes Kubernetes { get; private set; } 17 | private readonly IOperationHandler m_handler; 18 | private readonly T m_crd; 19 | private Watcher? m_watcher = null; 20 | private string m_k8sNamespace; 21 | 22 | static Controller() 23 | { 24 | ConfigLogger(); 25 | } 26 | 27 | static bool s_loggerConfiged = false; 28 | 29 | public static void ConfigLogger() 30 | { 31 | if (!s_loggerConfiged) 32 | { 33 | var config = new LoggingConfiguration(); 34 | var consoleTarget = new ColoredConsoleTarget 35 | { 36 | Name = "coloredConsole", 37 | Layout = "${longdate} [${level:uppercase=true}] ${logger}:${message}", 38 | }; 39 | config.AddRule(LogLevel.Debug, LogLevel.Fatal, consoleTarget, "*"); 40 | LogManager.Configuration = config; 41 | 42 | s_loggerConfiged = true; 43 | } 44 | } 45 | 46 | public Controller(T crd, IOperationHandler handler, string k8sNamespace = "") 47 | { 48 | KubernetesClientConfiguration config; 49 | if (KubernetesClientConfiguration.IsInCluster()) 50 | { 51 | config = KubernetesClientConfiguration.InClusterConfig(); 52 | } 53 | else 54 | { 55 | config = KubernetesClientConfiguration.BuildConfigFromConfigFile(); 56 | } 57 | 58 | Kubernetes = new Kubernetes(config); 59 | m_k8sNamespace = k8sNamespace; 60 | m_crd = crd; 61 | m_handler = handler; 62 | } 63 | 64 | ~Controller() 65 | { 66 | DisposeWatcher(); 67 | } 68 | 69 | private async Task IsCRDAvailable() 70 | { 71 | try 72 | { 73 | await Kubernetes.ListNamespacedCustomObjectWithHttpMessagesAsync(m_crd.Group, m_crd.Version, m_k8sNamespace, m_crd.Plural); 74 | } 75 | catch (HttpOperationException hoex) when (hoex.Response.StatusCode == System.Net.HttpStatusCode.NotFound) 76 | { 77 | Log.Warn($"No CustomResourceDefinition found for '{m_crd.Plural}', group '{m_crd.Group}' and version '{m_crd.Version}' on namespace '{m_k8sNamespace}'."); 78 | Log.Info($"Checking again in {m_crd.ReconciliationCheckInterval} seconds..."); 79 | 80 | return false; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | public async Task SatrtAsync() 87 | { 88 | while(! await IsCRDAvailable()) 89 | Thread.Sleep(m_crd.ReconciliationCheckInterval * 1000); 90 | 91 | StartWatcher(); 92 | await ReconciliationLoop(); 93 | } 94 | 95 | private void StartWatcher() 96 | { 97 | Task>? listResponse = null; 98 | DisposeWatcher(); 99 | 100 | listResponse = Kubernetes.ListNamespacedCustomObjectWithHttpMessagesAsync(m_crd.Group, m_crd.Version, m_k8sNamespace, m_crd.Plural, watch: true); 101 | 102 | m_watcher = listResponse.Watch(this.OnTChange, this.OnError, this.OnClose); 103 | } 104 | 105 | private Task ReconciliationLoop() 106 | { 107 | return Task.Run(() => 108 | { 109 | Log.Info($"Reconciliation Loop for CRD {m_crd.Singular} will run every {m_crd.ReconciliationCheckInterval} seconds."); 110 | 111 | while (true) 112 | { 113 | Thread.Sleep(m_crd.ReconciliationCheckInterval * 1000); 114 | 115 | m_handler.CheckCurrentState(Kubernetes); 116 | } 117 | }); 118 | } 119 | 120 | void DisposeWatcher() 121 | { 122 | if (m_watcher != null && m_watcher.Watching) 123 | m_watcher.Dispose(); 124 | } 125 | 126 | private async void OnTChange(WatchEventType type, T item) 127 | { 128 | Log.Info($"{typeof(T)} {item.Name()} {type} on Namespace {item.Namespace()}"); 129 | 130 | try 131 | { 132 | switch (type) 133 | { 134 | case WatchEventType.Added: 135 | if (m_handler != null) 136 | await m_handler.OnAdded(Kubernetes, item); 137 | return; 138 | case WatchEventType.Modified: 139 | if (m_handler != null) 140 | await m_handler.OnUpdated(Kubernetes, item); 141 | return; 142 | case WatchEventType.Deleted: 143 | if (m_handler != null) 144 | await m_handler.OnDeleted(Kubernetes, item); 145 | return; 146 | case WatchEventType.Bookmark: 147 | if (m_handler != null) 148 | await m_handler.OnBookmarked(Kubernetes, item); 149 | return; 150 | case WatchEventType.Error: 151 | if (m_handler != null) 152 | await m_handler.OnError(Kubernetes, item); 153 | return; 154 | default: 155 | Log.Warn($"Don't know what to do with {type}"); 156 | break; 157 | }; 158 | } 159 | catch (Exception ex) 160 | { 161 | Log.Error($"An error occurred on the '{type}' call of {item.Name()} ({typeof(T)})"); 162 | Log.Error(ex); 163 | } 164 | } 165 | 166 | private void OnError(Exception exception) 167 | { 168 | if (exception is TaskCanceledException) 169 | { 170 | TaskCanceledException? tcex = exception as TaskCanceledException; 171 | if (tcex?.InnerException != null && tcex.InnerException is TimeoutException) 172 | { 173 | DisposeWatcher(); 174 | return; 175 | } 176 | 177 | } 178 | Log.Fatal(exception); 179 | } 180 | 181 | private void OnClose() 182 | { 183 | Log.Fatal($"Connection Closed. Restarting {m_crd.Plural} Operator"); 184 | SatrtAsync().GetAwaiter().GetResult(); 185 | } 186 | } -------------------------------------------------------------------------------- /Kncs.Webhook/manifests/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: kncs-system 6 | 7 | 8 | --- 9 | apiVersion: v1 10 | kind: Secret 11 | metadata: 12 | name: kncs-server-certs 13 | namespace: kncs-system 14 | type: kubernetes.io/tls 15 | data: 16 | tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUV6VENDQXJXZ0F3SUJBZ0lKQU4wTjdiTGtxM1hSTUEwR0NTcUdTSWIzRFFFQkN3VUFNRzh4Q3pBSkJnTlYKQkFZVEFrTk9NUkF3RGdZRFZRUUlEQWRDWldscWFXNW5NUkV3RHdZRFZRUUhEQWhEYUdGdmVXRnVaekVQTUEwRwpBMVVFQ2d3R1JHVjJUM0J6TVF3d0NnWURWUVFMREFOUVMwa3hIREFhQmdOVkJBTU1FMFJsZGs5d2N5QkhaVzVsCmNtbGpJRkp2YjNRd0hoY05Nakl3TlRFMU1EZzBNRE15V2hjTk1qTXdOVEUxTURnME1ETXlXakI0TVFzd0NRWUQKVlFRR0V3SkRUakVSTUE4R0ExVUVDQXdJVTJobGJucG9aVzR4RURBT0JnTlZCQWNNQjA1aGJuTm9ZVzR4RHpBTgpCZ05WQkFvTUJrUmxkazl3Y3pFTU1Bb0dBMVVFQ3d3RFVFdEpNU1V3SXdZRFZRUUREQnhyYm1OekxYZGxZbWh2CmIyc3VhMjVqY3kxemVYTjBaVzB1YzNaak1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0MKQVFFQXV5aTJwdGM3NWVxVG5BWDRHQnZmUXpqOGt3SWVQcVRIU3hVOWtpWFpKL1JMUWdheHBUUmROL3F0SkNFVwpaSUhOUHFJVU5wNnRpeXF4d3poQzR5UHNkS3JIREl4eGQreHFPMEp3VmNMNUk4dEhsT0RIY0pJUDM2L1ZKS0VBClNlT2lqMm91T3pkYXBZRFh2dm5wVkdMQzRkKzNiTVM5cXNVMjFUS2VqU1VNZGZhdVdObXdzNGk1eVRlVVA1WUkKeWdBZWRYRGs5ZWM2WmtUNTBFV2JhU1VQdkNEWEZHY3NnZVQxSlNkclgxemw5dXl3bjFZa1h2d3gyd0VYdTlHUApqdVRMYmxGSEkzYnRKTTJVNldkbXJvTWlESmY4V1dPeXNCdDMvczVjZWduRTUrSDgxVzV0ZWR4WWc3MUMvUzVmCjgrWFpsbG9GL3FteVhwZnhEM0x6QUtFbGV3SURBUUFCbzJNd1lUQUpCZ05WSFJNRUFqQUFNQXNHQTFVZER3UUUKQXdJRjREQkhCZ05WSFJFRVFEQStnaHhyYm1OekxYZGxZbWh2YjJzdWEyNWpjeTF6ZVhOMFpXMHVjM1pqZ2g0cQpMbXR1WTNNdGQyVmlhRzl2YXk1cmJtTnpMWE41YzNSbGJTNXpkbU13RFFZSktvWklodmNOQVFFTEJRQURnZ0lCCkFKTmlEbGp0WEM4RldPT0dsMmdnODg3RExLTmROQ3NuYzhRRUR3NXNJWUhUN2FkSFAzQlRPSG0xdGN6dW5uL2sKWEVTVEd1MzQwZmhpanljeXViQjJsaCsvYXBKZDNPQkNwdkNjLzg0c3l5Z3lnenFUWGxKUmowUzBiaWhWQ2xLNwpnZHVSYWpxQnpBWE55RkcrM25LWkpEQVk3NklMNlVFejdzWXlFSTdsMlFtQmN4VTM5VmJuU0dnMCt1czZhWWtnCjF6WXA3c3NUcEpMS013cHZYOG96TnY1TjFKOGFHVjJJYldUdHh2cUR1N01xZ2FHbzBlZUl2K3NwNVRSRiswWUoKYXBXS1Bzd2wxMW1hMEpPS1Z5RG8zOEUrT3Q1K2FFdjF6V3BEaGU0NHpaOVB4U01uaGVTMTBySHRFdkZ4bmxpWApGeWlwVkJBK2lFWUQ2b0ZVeWIxVzJUN2Zna1M2UWdrSzVQeGRITFg3U2VqaHh3OTRzbzllWjBqSGZLd012WWNwCktHenhnOUJYMnpKSmcwM0sramUwemhIenkzMy9KYVlzVzlXVTF1UjlqenlvMWtMWFZDZWhXNlFlbDV6bzFMKzAKYkZPVHc3bnhtTW1sdTJ6NTFUVld0OHlvUWdWVEJzRFFxZDg1MlVHMGwzYVp6ZFlLUmN6MlpwZ3VqVjRCZjlmaAp0RjFXNXZsYnlPR05wM0ZqV2NkdTNxZW5PU0RtaUd3V0dYZjNKK0o4bmUwa0dKajBabHljekxTbUdHYlo1YlBGClZIaWJISlc4YnAwMGpYNVVTVFY4blQxLzVFT3k3blRpRUJXRDZpNE1SUGl5ZmwvNHVGdGFUeTlNRm5KRDYrSnQKWGVmR3VvWXVFUDNqN1RNWmJYZnJzSFk1dDcvZGV1bldyWVh6K3JkQ25PVUEKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= 17 | tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdXlpMnB0Yzc1ZXFUbkFYNEdCdmZRemo4a3dJZVBxVEhTeFU5a2lYWkovUkxRZ2F4CnBUUmROL3F0SkNFV1pJSE5QcUlVTnA2dGl5cXh3emhDNHlQc2RLckhESXh4ZCt4cU8wSndWY0w1STh0SGxPREgKY0pJUDM2L1ZKS0VBU2VPaWoyb3VPemRhcFlEWHZ2bnBWR0xDNGQrM2JNUzlxc1UyMVRLZWpTVU1kZmF1V05tdwpzNGk1eVRlVVA1WUl5Z0FlZFhEazllYzZaa1Q1MEVXYmFTVVB2Q0RYRkdjc2dlVDFKU2RyWDF6bDl1eXduMVlrClh2d3gyd0VYdTlHUGp1VExibEZISTNidEpNMlU2V2Rtcm9NaURKZjhXV095c0J0My9zNWNlZ25FNStIODFXNXQKZWR4WWc3MUMvUzVmOCtYWmxsb0YvcW15WHBmeEQzTHpBS0VsZXdJREFRQUJBb0lCQUQwUE1rL0tKbk9ERFRjNAp4MUR1UHUrS2R2UnJHM3pxZTA1bWxwaklta2tyclNYVVV6NkhqK1lFZFZvMUpUNFREdWZoTHVFRzhhMVdkM291Ckw3dzA2eDdBM0lHZWpDSkkwZnVWV0ZyU2FqK2dRVEUwQ0QwVW1mTXJSVWxXOFdZcHlzNHBJUDRXdUE4SXN0cE8KWkM0d3JrM01rK1g3WmJtQjc3cXNjZ2V3VDVsb0d6OGdKNG14dVF5MnR5SWF6d3VUQnBkbm5VUWh0QkJLZ2J1YwplYnpTS0luZ3RXa2o3SHlDWmVYY0xTdmpORzNGcEpWUXZZK1pncC9rbno1S042RFZXUkZNWThFV3BpTVVaVE1HCmVVVCtuSER4NkZlSVllNVBhRG41TWROTWhPOGZRYWdBVG9GcDRkRU5xb0l6RWVIWkZwOEJTODgwTXpTQSt0UTgKanNMaEt1RUNnWUVBNXYvTFptNXVaQ1RwNnY0NXJjQlRIM2VadXVmOVJ1Z01HYXJMS2tTS1RHZGZLMEg3dlVMZQpBSDlxalVuMExtSUxscVdmcHJiMC90eVBTUEFaSVhYY0FYQVA1TDJBdlJNNnQ1c0JXWk5HcnV5aVcyWkxTQzhQCnROQ3RHTkxmK0ZGQzJnZjlBTzZtZjhFRzlkYW9uNFdvKzZWU1FYMlRiOUZMYkxHWnByeDZjL0VDZ1lFQXoycEIKcnU1TE1ENS9abHpwY2xJNGg4RytpZ3dDcjM4K3V1NmdXRmZFZVRkYjk0cm1RclptM2xPQkxqVTBteExUTlUrVQp3NEVVQ3piR2xyYlNWRVZZL0s0Zi9ZZXBXazB4cjY3aUN1amd5ZzVSbDM5WFlMYXhxMVkvUzZ0TlNQdVQ2RjYzCkJMY1RYM3YrQ2RDR0Q5L0V1TWFEcTBJM0h5TjlOVkVPaCsrR2JDc0NnWUVBMk9MZ3Vnc0RrUGxydTl6NGtOL2IKNjlhaXUyK29TZFFEMEhHaEVjMkt3RlBxY2pZZ3c4R3RxVy80dmpIcWwwWXROVVBLazRDQ3BXeTNCN2VQRVBDVgpJYkJ5NjhUVnhERHkxNE10RUVxTWVoN3FEY0VNKy9oYjJkeDRPYTk4NUt4L2hURXM1cHdzTGhVeGtNNzhRZE1BCkowNUEzZ2FtMEwwRkFVZjdTU2I4SGpFQ2dZQTUrN2xxL3NEVU50U0V1RHFtcytlTHhCVFJJTFJyZlVYN0doU0gKUGRuMkRRelBzZXZYQUlqWFpENjd2VEg4bkJHaFdLTDgySXZTNnJndmorSlNucVJXMXhLb1hKRnlaaHdhd2VmOQpKc2NZbFZJbjZQaHpWLzlwSjQ1QVNCNHQ1ZTZlU2tRZHRGUmRJQnVQZ05USmdVUE1aK3FOS05DaUN0akkyK1VWCkNWZnB5d0tCZ0hvbERiZVRvbTZKbHppcnN0TXFlNUZwZ0NBQ0VIcDR1dkw1Tjk4TmlzWmNPRm05d1ZKcnErYVIKUVJDT1pXRnY0OTM1a2tFLzJnRDVzRnZYZzUzZmVjbmV2UFFWRytsbU1sUXRoSlR0NzRpTkZpRlBqRDR5Y0lDNQpCZG5yWkNDTWJMWHVUdEhiZmtIbXphRlExaUFKUTV0SkN3dG04TFVEd0YvMzM2bk5FN1RYCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== 18 | 19 | 20 | --- 21 | 22 | apiVersion: apps/v1 23 | kind: Deployment 24 | metadata: 25 | labels: 26 | app: kncs-webhook 27 | kncs: webhook 28 | name: kncs-webhook 29 | namespace: kncs-system 30 | spec: 31 | progressDeadlineSeconds: 600 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | kncs: webhook 36 | strategy: 37 | rollingUpdate: 38 | maxSurge: 100% 39 | maxUnavailable: 25% 40 | type: RollingUpdate 41 | template: 42 | metadata: 43 | annotations: 44 | sidecar.istio.io/inject: "false" 45 | k8s.jijiechen.com/inject-dotnet-helper: "false" 46 | labels: 47 | kncs: webhook 48 | spec: 49 | containers: 50 | - env: 51 | - name: BLOCKED_REPOS 52 | value: abcd.com 53 | - name: ASPNETCORE_URLS 54 | value: 'https://+:443' 55 | - name: ASPNETCORE_Kestrel__Certificates__Default__Path 56 | value: /etc/kncs/certs/tls.crt 57 | - name: ASPNETCORE_Kestrel__Certificates__Default__KeyPath 58 | value: /etc/kncs/certs/tls.key 59 | # - name: Logging__LogLevel__Default 60 | # value: Debug 61 | # - name: Logging__LogLevel__Microsoft.AspNetCore 62 | # value: Debug 63 | # - name: DEBUG 64 | # value: 'true' 65 | # - name: Microsoft__AspNetCore__HttpLogging__HttpLoggingMiddleware 66 | # value: Information 67 | image: jijiechen-docker.pkg.coding.net/sharpcr/kncs/webhook:0515 68 | imagePullPolicy: IfNotPresent 69 | name: webhook 70 | ports: 71 | - containerPort: 443 72 | protocol: TCP 73 | resources: 74 | requests: 75 | cpu: 50m 76 | memory: 128Mi 77 | volumeMounts: 78 | - mountPath: /etc/kncs/certs 79 | name: certs 80 | restartPolicy: Always 81 | schedulerName: default-scheduler 82 | volumes: 83 | - name: certs 84 | secret: 85 | defaultMode: 420 86 | secretName: kncs-server-certs 87 | 88 | 89 | --- 90 | 91 | apiVersion: v1 92 | kind: Service 93 | metadata: 94 | labels: 95 | app: kncs-webhook 96 | name: kncs-webhook 97 | namespace: kncs-system 98 | spec: 99 | ports: 100 | - name: https 101 | port: 443 102 | protocol: TCP 103 | targetPort: 443 104 | selector: 105 | kncs: webhook 106 | sessionAffinity: None 107 | type: ClusterIP 108 | 109 | 110 | 111 | --- 112 | 113 | apiVersion: admissionregistration.k8s.io/v1 114 | kind: ValidatingWebhookConfiguration 115 | metadata: 116 | name: validating-webhook-kncs 117 | webhooks: 118 | - admissionReviewVersions: 119 | - v1beta1 120 | - v1 121 | clientConfig: 122 | caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZzVENDQTVtZ0F3SUJBZ0lKQU1LNnNwL3N3Uzl5TUEwR0NTcUdTSWIzRFFFQkN3VUFNRzh4Q3pBSkJnTlYKQkFZVEFrTk9NUkF3RGdZRFZRUUlEQWRDWldscWFXNW5NUkV3RHdZRFZRUUhEQWhEYUdGdmVXRnVaekVQTUEwRwpBMVVFQ2d3R1JHVjJUM0J6TVF3d0NnWURWUVFMREFOUVMwa3hIREFhQmdOVkJBTU1FMFJsZGs5d2N5QkhaVzVsCmNtbGpJRkp2YjNRd0hoY05Nakl3TlRFMU1EZzBNRE14V2hjTk16SXdOVEV5TURnME1ETXhXakJ2TVFzd0NRWUQKVlFRR0V3SkRUakVRTUE0R0ExVUVDQXdIUW1WcGFtbHVaekVSTUE4R0ExVUVCd3dJUTJoaGIzbGhibWN4RHpBTgpCZ05WQkFvTUJrUmxkazl3Y3pFTU1Bb0dBMVVFQ3d3RFVFdEpNUnd3R2dZRFZRUUREQk5FWlhaUGNITWdSMlZ1ClpYSnBZeUJTYjI5ME1JSUNJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBZzhBTUlJQ0NnS0NBZ0VBeFZHdUwrcnoKOGJXWWtoME5SbDUyOHJFcHlhNmVoU2tPdUU1QWN6MERGMGJCSjRyZzVCaE8xS1NyWlhzd1krVFp6c1ZqL0pKbwpWYnd5YXdxcTZRZ2tXWEhUTHcya3d3UE5MQ1RYakk4TUZ0TUlKdkdYaktJZnp6aXE1c2hxYVVWWU8vNm13WFBmCjdTMVhVOFFrelErNnJ0aEgvUFdYYkFjMWlRbXhvWGgvVXVRZkRQMHcrM3djelZwc0VYSm5xVmFnYUlBbDdSbXkKMEhYM0JoV281WTQwMFFVSEF3bnJpUUVXWmRhYUEwaExGaFZqMHl4WW5HUnBQOWJjUEZmc0poOUFyam04M2pSRQp3RU02MVFJOU81TExmMlJTTlFmMnB5ZmNKZm1KZWVvK2pVQTEwZjJWOC8wY3YvRGJVZitjZDFRWGhUQllEek9iCnlNWWZsMUFIMGIxZit0YU9WYlFmT0QwalRyWXNLS1hrYmtYNUpPZzBXMndvd3VYbURXa1A1aGl3MUZNRGN5MngKU0dsRG8wQjNQMXVCSkJzc0ErUXlvT05YWDBoYWRsekR0NDhpbk5hdTF5L1Exb1daMHJvZDFBa0t0TnJBRk1WYQpxekhVRVRtTDNaRUtXbDFwbjBadE40ZUNVSEZhOGZDZ0MwVDUvK0VFU1ZoV1dPQ2FjU1VJbkIyUFlxZnBUWnlMCmZ6UnRRR2hYcE1xcWVCbDNvcjJtUUI5REZKdHZNZWl4Y3VHTVdSTGIyOWkxKy9qVkdzVTU5ck9tTDJIaXJsTjIKU2Izcmd5RkpXb3FyQmN5L215dEJ5VG9sc1RPc1BTRGRORE81ZGRiVkVLMk9IZC9YZ29MUTh1TVpCUng4Y3Y1dQo1a0grNS9xanRGbi9rcDg0Sy9yY0k2T2V4eWFielRsQm51Y0NBd0VBQWFOUU1FNHdIUVlEVlIwT0JCWUVGQnlmCmllYnU3N3pHQ1lwbU9EU3pmWmMzUXRuQ01COEdBMVVkSXdRWU1CYUFGQnlmaWVidTc3ekdDWXBtT0RTemZaYzMKUXRuQ01Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0lCQUNCZkpzMXNtYnZDUHYxYQorcWZQTy8vbmpPTHBOVm1ybzZZcVp6SkJqbHo0VmpVZ1JOMGxrQ1ltTk9oM0orRnVMRGNwMi9XRjJ5RnlkZlhLClBORGZQb2xkQ2RrdHh5OVk2SlpHM3dOQ3pqTEdJWVpwZStET2RjSmswellqYy9HRWFNTkVmQVVJN09zSFlDcTUKLy8ycWNQTWpOOG1TTnJiM0FwZkZaNmJsMUFGSW9NNzBZUUFDbHRKTWdkNFRSSWhISlBibmdaVjE0b1kyNWp6UApscFBMUVVRV1B2V00wbm43QW1OMldmdG5mNGl2ZzludHczN1pvdm1nc0JhaHhockRCWTBUbmdYNWFlVElHOEFICmFvZjhneUxvUVIzQjhZeURBU2N5Y0l0ZUhNdDgrNVp3YkdFTVdJanVFNUdKcUtBeWZDOUlIWjdFMHBIbUg2ck0KY1hoaVFNTjdLZm1LaGwvbGZtbmlIeEpTNnE2eUlsbmU4NDRpK0l3Y3crL0sxbUJDdFZDZ0R4OUc3TWo2QUljeApWekJyYlVrSUdQKzRxc2NmbDRGTDRIbGdjOE9vMnpYbmc1T2VpQ2JTSGRXK21zMFdXeVM1cGRZd29BbXFzaisyCnYzdmtMS3llR0lWNHBscWl4ZjMybXdrTW9NTXRPQ1NNR3FzZmhwTFFLUUhSaXNxMjNhc0tOYVZXV0RtcHFqa2YKbmZPWFJKdkpDbktFMXRJNkpPbjViN3pCc1JjaC9maVRRcmZ3Zk1sU3N2N2ZCRGZEbE8zU09STkhHTU0zRndUdgpZYjhsMllaTEpTNmpoeVZsUVZESjRUVW5TeXZJVURxOWJhZVJKTU1vZ3g2QmhTY2tGbFhwRmJiT2FVMmMzaktsCmtlUFVKZEUzU1ZnbWt6RW10YmJVTHdnOENPV0UKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= 123 | service: 124 | name: kncs-webhook 125 | namespace: kncs-system 126 | path: /validate 127 | port: 443 128 | failurePolicy: Fail 129 | name: image-validator.kncs.io 130 | namespaceSelector: {} 131 | objectSelector: {} 132 | rules: 133 | - apiGroups: 134 | - "" 135 | apiVersions: 136 | - v1 137 | operations: 138 | - CREATE 139 | - UPDATE 140 | - DELETE 141 | resources: 142 | - pods 143 | scope: '*' 144 | sideEffects: None 145 | timeoutSeconds: 30 146 | 147 | --- 148 | 149 | apiVersion: admissionregistration.k8s.io/v1 150 | kind: MutatingWebhookConfiguration 151 | metadata: 152 | name: mutating-webhook-kncs 153 | webhooks: 154 | - admissionReviewVersions: 155 | - v1beta1 156 | - v1 157 | clientConfig: 158 | caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZzVENDQTVtZ0F3SUJBZ0lKQU1LNnNwL3N3Uzl5TUEwR0NTcUdTSWIzRFFFQkN3VUFNRzh4Q3pBSkJnTlYKQkFZVEFrTk9NUkF3RGdZRFZRUUlEQWRDWldscWFXNW5NUkV3RHdZRFZRUUhEQWhEYUdGdmVXRnVaekVQTUEwRwpBMVVFQ2d3R1JHVjJUM0J6TVF3d0NnWURWUVFMREFOUVMwa3hIREFhQmdOVkJBTU1FMFJsZGs5d2N5QkhaVzVsCmNtbGpJRkp2YjNRd0hoY05Nakl3TlRFMU1EZzBNRE14V2hjTk16SXdOVEV5TURnME1ETXhXakJ2TVFzd0NRWUQKVlFRR0V3SkRUakVRTUE0R0ExVUVDQXdIUW1WcGFtbHVaekVSTUE4R0ExVUVCd3dJUTJoaGIzbGhibWN4RHpBTgpCZ05WQkFvTUJrUmxkazl3Y3pFTU1Bb0dBMVVFQ3d3RFVFdEpNUnd3R2dZRFZRUUREQk5FWlhaUGNITWdSMlZ1ClpYSnBZeUJTYjI5ME1JSUNJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBZzhBTUlJQ0NnS0NBZ0VBeFZHdUwrcnoKOGJXWWtoME5SbDUyOHJFcHlhNmVoU2tPdUU1QWN6MERGMGJCSjRyZzVCaE8xS1NyWlhzd1krVFp6c1ZqL0pKbwpWYnd5YXdxcTZRZ2tXWEhUTHcya3d3UE5MQ1RYakk4TUZ0TUlKdkdYaktJZnp6aXE1c2hxYVVWWU8vNm13WFBmCjdTMVhVOFFrelErNnJ0aEgvUFdYYkFjMWlRbXhvWGgvVXVRZkRQMHcrM3djelZwc0VYSm5xVmFnYUlBbDdSbXkKMEhYM0JoV281WTQwMFFVSEF3bnJpUUVXWmRhYUEwaExGaFZqMHl4WW5HUnBQOWJjUEZmc0poOUFyam04M2pSRQp3RU02MVFJOU81TExmMlJTTlFmMnB5ZmNKZm1KZWVvK2pVQTEwZjJWOC8wY3YvRGJVZitjZDFRWGhUQllEek9iCnlNWWZsMUFIMGIxZit0YU9WYlFmT0QwalRyWXNLS1hrYmtYNUpPZzBXMndvd3VYbURXa1A1aGl3MUZNRGN5MngKU0dsRG8wQjNQMXVCSkJzc0ErUXlvT05YWDBoYWRsekR0NDhpbk5hdTF5L1Exb1daMHJvZDFBa0t0TnJBRk1WYQpxekhVRVRtTDNaRUtXbDFwbjBadE40ZUNVSEZhOGZDZ0MwVDUvK0VFU1ZoV1dPQ2FjU1VJbkIyUFlxZnBUWnlMCmZ6UnRRR2hYcE1xcWVCbDNvcjJtUUI5REZKdHZNZWl4Y3VHTVdSTGIyOWkxKy9qVkdzVTU5ck9tTDJIaXJsTjIKU2Izcmd5RkpXb3FyQmN5L215dEJ5VG9sc1RPc1BTRGRORE81ZGRiVkVLMk9IZC9YZ29MUTh1TVpCUng4Y3Y1dQo1a0grNS9xanRGbi9rcDg0Sy9yY0k2T2V4eWFielRsQm51Y0NBd0VBQWFOUU1FNHdIUVlEVlIwT0JCWUVGQnlmCmllYnU3N3pHQ1lwbU9EU3pmWmMzUXRuQ01COEdBMVVkSXdRWU1CYUFGQnlmaWVidTc3ekdDWXBtT0RTemZaYzMKUXRuQ01Bd0dBMVVkRXdRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0lCQUNCZkpzMXNtYnZDUHYxYQorcWZQTy8vbmpPTHBOVm1ybzZZcVp6SkJqbHo0VmpVZ1JOMGxrQ1ltTk9oM0orRnVMRGNwMi9XRjJ5RnlkZlhLClBORGZQb2xkQ2RrdHh5OVk2SlpHM3dOQ3pqTEdJWVpwZStET2RjSmswellqYy9HRWFNTkVmQVVJN09zSFlDcTUKLy8ycWNQTWpOOG1TTnJiM0FwZkZaNmJsMUFGSW9NNzBZUUFDbHRKTWdkNFRSSWhISlBibmdaVjE0b1kyNWp6UApscFBMUVVRV1B2V00wbm43QW1OMldmdG5mNGl2ZzludHczN1pvdm1nc0JhaHhockRCWTBUbmdYNWFlVElHOEFICmFvZjhneUxvUVIzQjhZeURBU2N5Y0l0ZUhNdDgrNVp3YkdFTVdJanVFNUdKcUtBeWZDOUlIWjdFMHBIbUg2ck0KY1hoaVFNTjdLZm1LaGwvbGZtbmlIeEpTNnE2eUlsbmU4NDRpK0l3Y3crL0sxbUJDdFZDZ0R4OUc3TWo2QUljeApWekJyYlVrSUdQKzRxc2NmbDRGTDRIbGdjOE9vMnpYbmc1T2VpQ2JTSGRXK21zMFdXeVM1cGRZd29BbXFzaisyCnYzdmtMS3llR0lWNHBscWl4ZjMybXdrTW9NTXRPQ1NNR3FzZmhwTFFLUUhSaXNxMjNhc0tOYVZXV0RtcHFqa2YKbmZPWFJKdkpDbktFMXRJNkpPbjViN3pCc1JjaC9maVRRcmZ3Zk1sU3N2N2ZCRGZEbE8zU09STkhHTU0zRndUdgpZYjhsMllaTEpTNmpoeVZsUVZESjRUVW5TeXZJVURxOWJhZVJKTU1vZ3g2QmhTY2tGbFhwRmJiT2FVMmMzaktsCmtlUFVKZEUzU1ZnbWt6RW10YmJVTHdnOENPV0UKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= 159 | service: 160 | name: kncs-webhook 161 | namespace: kncs-system 162 | path: /mutate 163 | port: 443 164 | failurePolicy: Fail 165 | name: dotnet-helper-injector.kncs.io 166 | namespaceSelector: {} 167 | objectSelector: {} 168 | rules: 169 | - apiGroups: 170 | - "" 171 | apiVersions: 172 | - v1 173 | operations: 174 | - CREATE 175 | - UPDATE 176 | - DELETE 177 | resources: 178 | - pods 179 | scope: '*' 180 | sideEffects: None 181 | timeoutSeconds: 30 182 | 183 | 184 | -------------------------------------------------------------------------------- /Kncs.Webhook/AdminReviewModels/admission.swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "k8s.io/api/admission/v1", 5 | "version": "1.0" 6 | }, 7 | "paths": {}, 8 | "definitions": { 9 | "runtime.RawExtension": { 10 | "description": "RawExtension is used to hold extensions in external versions.\n\nTo use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types.\n\n// Internal package: type MyAPIObject struct {\n\truntime.TypeMeta `json:\",inline\"`\n\tMyPlugin runtime.Object `json:\"myPlugin\"`\n} type PluginA struct {\n\tAOption string `json:\"aOption\"`\n}\n\n// External package: type MyAPIObject struct {\n\truntime.TypeMeta `json:\",inline\"`\n\tMyPlugin runtime.RawExtension `json:\"myPlugin\"`\n} type PluginA struct {\n\tAOption string `json:\"aOption\"`\n}\n\n// On the wire, the JSON will look something like this: {\n\t\"kind\":\"MyAPIObject\",\n\t\"apiVersion\":\"v1\",\n\t\"myPlugin\": {\n\t\t\"kind\":\"PluginA\",\n\t\t\"aOption\":\"foo\",\n\t},\n}\n\nSo what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.)", 11 | "type": "object" 12 | }, 13 | "v1.AdmissionRequest": { 14 | "description": "AdmissionRequest describes the admission.Attributes for the admission request.", 15 | "type": "object", 16 | "required": [ 17 | "uid", 18 | "kind", 19 | "resource", 20 | "operation", 21 | "userInfo" 22 | ], 23 | "properties": { 24 | "dryRun": { 25 | "description": "DryRun indicates that modifications will definitely not be persisted for this request. Defaults to false.", 26 | "type": "boolean" 27 | }, 28 | "kind": { 29 | "description": "Kind is the fully-qualified type of object being submitted (for example, v1.Pod or autoscaling.v1.Scale)", 30 | "$ref": "#/definitions/v1.GroupVersionKind" 31 | }, 32 | "name": { 33 | "description": "Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and rely on the server to generate the name. If that is the case, this field will contain an empty string.", 34 | "type": "string" 35 | }, 36 | "namespace": { 37 | "description": "Namespace is the namespace associated with the request (if any).", 38 | "type": "string" 39 | }, 40 | "object": { 41 | "description": "Object is the object from the incoming request.", 42 | "$ref": "#/definitions/runtime.RawExtension" 43 | }, 44 | "oldObject": { 45 | "description": "OldObject is the existing object. Only populated for DELETE and UPDATE requests.", 46 | "$ref": "#/definitions/runtime.RawExtension" 47 | }, 48 | "operation": { 49 | "description": "Operation is the operation being performed. This may be different than the operation requested. e.g. a patch can result in either a CREATE or UPDATE Operation.", 50 | "type": "string" 51 | }, 52 | "options": { 53 | "description": "Options is the operation option structure of the operation being performed. e.g. `meta.k8s.io/v1.DeleteOptions` or `meta.k8s.io/v1.CreateOptions`. This may be different than the options the caller provided. e.g. for a patch request the performed Operation might be a CREATE, in which case the Options will a `meta.k8s.io/v1.CreateOptions` even though the caller provided `meta.k8s.io/v1.PatchOptions`.", 54 | "$ref": "#/definitions/runtime.RawExtension" 55 | }, 56 | "requestKind": { 57 | "description": "RequestKind is the fully-qualified type of the original API request (for example, v1.Pod or autoscaling.v1.Scale). If this is specified and differs from the value in \"kind\", an equivalent match and conversion was performed.\n\nFor example, if deployments can be modified via apps/v1 and apps/v1beta1, and a webhook registered a rule of `apiGroups:[\"apps\"], apiVersions:[\"v1\"], resources: [\"deployments\"]` and `matchPolicy: Equivalent`, an API request to apps/v1beta1 deployments would be converted and sent to the webhook with `kind: {group:\"apps\", version:\"v1\", kind:\"Deployment\"}` (matching the rule the webhook registered for), and `requestKind: {group:\"apps\", version:\"v1beta1\", kind:\"Deployment\"}` (indicating the kind of the original API request).\n\nSee documentation for the \"matchPolicy\" field in the webhook configuration type for more details.", 58 | "$ref": "#/definitions/v1.GroupVersionKind" 59 | }, 60 | "requestResource": { 61 | "description": "RequestResource is the fully-qualified resource of the original API request (for example, v1.pods). If this is specified and differs from the value in \"resource\", an equivalent match and conversion was performed.\n\nFor example, if deployments can be modified via apps/v1 and apps/v1beta1, and a webhook registered a rule of `apiGroups:[\"apps\"], apiVersions:[\"v1\"], resources: [\"deployments\"]` and `matchPolicy: Equivalent`, an API request to apps/v1beta1 deployments would be converted and sent to the webhook with `resource: {group:\"apps\", version:\"v1\", resource:\"deployments\"}` (matching the resource the webhook registered for), and `requestResource: {group:\"apps\", version:\"v1beta1\", resource:\"deployments\"}` (indicating the resource of the original API request).\n\nSee documentation for the \"matchPolicy\" field in the webhook configuration type.", 62 | "$ref": "#/definitions/v1.GroupVersionResource" 63 | }, 64 | "requestSubResource": { 65 | "description": "RequestSubResource is the name of the subresource of the original API request, if any (for example, \"status\" or \"scale\") If this is specified and differs from the value in \"subResource\", an equivalent match and conversion was performed. See documentation for the \"matchPolicy\" field in the webhook configuration type.", 66 | "type": "string" 67 | }, 68 | "resource": { 69 | "description": "Resource is the fully-qualified resource being requested (for example, v1.pods)", 70 | "$ref": "#/definitions/v1.GroupVersionResource" 71 | }, 72 | "subResource": { 73 | "description": "SubResource is the subresource being requested, if any (for example, \"status\" or \"scale\")", 74 | "type": "string" 75 | }, 76 | "uid": { 77 | "description": "UID is an identifier for the individual request/response. It allows us to distinguish instances of requests which are otherwise identical (parallel requests, requests when earlier requests did not modify etc) The UID is meant to track the round trip (request/response) between the KAS and the WebHook, not the user request. It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging.", 78 | "type": "string" 79 | }, 80 | "userInfo": { 81 | "description": "UserInfo is information about the requesting user", 82 | "$ref": "#/definitions/v1.UserInfo" 83 | } 84 | } 85 | }, 86 | "v1.AdmissionResponse": { 87 | "description": "AdmissionResponse describes an admission response.", 88 | "type": "object", 89 | "required": [ 90 | "uid", 91 | "allowed" 92 | ], 93 | "properties": { 94 | "allowed": { 95 | "description": "Allowed indicates whether or not the admission request was permitted.", 96 | "type": "boolean" 97 | }, 98 | "auditAnnotations": { 99 | "description": "AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted). MutatingAdmissionWebhook and ValidatingAdmissionWebhook admission controller will prefix the keys with admission webhook name (e.g. imagepolicy.example.com/error=image-blacklisted). AuditAnnotations will be provided by the admission webhook to add additional context to the audit log for this request.", 100 | "type": "object", 101 | "additionalProperties": { 102 | "type": "string" 103 | } 104 | }, 105 | "patch": { 106 | "description": "The patch body. Currently we only support \"JSONPatch\" which implements RFC 6902.", 107 | "type": "string", 108 | "format": "byte" 109 | }, 110 | "patchType": { 111 | "description": "The type of Patch. Currently we only allow \"JSONPatch\".", 112 | "type": "string" 113 | }, 114 | "status": { 115 | "description": "Result contains extra details into why an admission request was denied. This field IS NOT consulted in any way if \"Allowed\" is \"true\".", 116 | "$ref": "#/definitions/v1.Status" 117 | }, 118 | "uid": { 119 | "description": "UID is an identifier for the individual request/response. This must be copied over from the corresponding AdmissionRequest.", 120 | "type": "string" 121 | }, 122 | "warnings": { 123 | "description": "warnings is a list of warning messages to return to the requesting API client. Warning messages describe a problem the client making the API request should correct or be aware of. Limit warnings to 120 characters if possible. Warnings over 256 characters and large numbers of warnings may be truncated.", 124 | "type": "array", 125 | "items": { 126 | "type": "string" 127 | } 128 | } 129 | } 130 | }, 131 | "v1.AdmissionReview": { 132 | "description": "AdmissionReview describes an admission review request/response.", 133 | "type": "object", 134 | "properties": { 135 | "apiVersion": { 136 | "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", 137 | "type": "string" 138 | }, 139 | "kind": { 140 | "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", 141 | "type": "string" 142 | }, 143 | "request": { 144 | "description": "Request describes the attributes for the admission request.", 145 | "$ref": "#/definitions/v1.AdmissionRequest" 146 | }, 147 | "response": { 148 | "description": "Response describes the attributes for the admission response.", 149 | "$ref": "#/definitions/v1.AdmissionResponse" 150 | } 151 | } 152 | }, 153 | "v1.GroupVersionKind": { 154 | "description": "GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling", 155 | "type": "object", 156 | "required": [ 157 | "group", 158 | "version", 159 | "kind" 160 | ], 161 | "properties": { 162 | "group": { 163 | "type": "string" 164 | }, 165 | "kind": { 166 | "type": "string" 167 | }, 168 | "version": { 169 | "type": "string" 170 | } 171 | } 172 | }, 173 | "v1.GroupVersionResource": { 174 | "description": "GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling", 175 | "type": "object", 176 | "required": [ 177 | "group", 178 | "version", 179 | "resource" 180 | ], 181 | "properties": { 182 | "group": { 183 | "type": "string" 184 | }, 185 | "resource": { 186 | "type": "string" 187 | }, 188 | "version": { 189 | "type": "string" 190 | } 191 | } 192 | }, 193 | "v1.ListMeta": { 194 | "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", 195 | "type": "object", 196 | "properties": { 197 | "continue": { 198 | "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", 199 | "type": "string" 200 | }, 201 | "remainingItemCount": { 202 | "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", 203 | "type": "integer", 204 | "format": "int64" 205 | }, 206 | "resourceVersion": { 207 | "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", 208 | "type": "string" 209 | }, 210 | "selfLink": { 211 | "description": "selfLink is a URL representing this object. Populated by the system. Read-only.\n\nDEPRECATED Kubernetes will stop propagating this field in 1.20 release and the field is planned to be removed in 1.21 release.", 212 | "type": "string" 213 | } 214 | } 215 | }, 216 | "v1.Status": { 217 | "description": "Status is a return value for calls that don't return other objects.", 218 | "type": "object", 219 | "properties": { 220 | "apiVersion": { 221 | "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", 222 | "type": "string" 223 | }, 224 | "code": { 225 | "description": "Suggested HTTP return code for this status, 0 if not set.", 226 | "type": "integer", 227 | "format": "int32" 228 | }, 229 | "details": { 230 | "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", 231 | "$ref": "#/definitions/v1.StatusDetails" 232 | }, 233 | "kind": { 234 | "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", 235 | "type": "string" 236 | }, 237 | "message": { 238 | "description": "A human-readable description of the status of this operation.", 239 | "type": "string" 240 | }, 241 | "metadata": { 242 | "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", 243 | "$ref": "#/definitions/v1.ListMeta" 244 | }, 245 | "reason": { 246 | "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", 247 | "type": "string" 248 | }, 249 | "status": { 250 | "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", 251 | "type": "string" 252 | } 253 | } 254 | }, 255 | "v1.StatusCause": { 256 | "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", 257 | "type": "object", 258 | "properties": { 259 | "field": { 260 | "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", 261 | "type": "string" 262 | }, 263 | "message": { 264 | "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", 265 | "type": "string" 266 | }, 267 | "reason": { 268 | "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", 269 | "type": "string" 270 | } 271 | } 272 | }, 273 | "v1.StatusDetails": { 274 | "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", 275 | "type": "object", 276 | "properties": { 277 | "causes": { 278 | "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", 279 | "type": "array", 280 | "items": { 281 | "$ref": "#/definitions/v1.StatusCause" 282 | } 283 | }, 284 | "group": { 285 | "description": "The group attribute of the resource associated with the status StatusReason.", 286 | "type": "string" 287 | }, 288 | "kind": { 289 | "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", 290 | "type": "string" 291 | }, 292 | "name": { 293 | "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", 294 | "type": "string" 295 | }, 296 | "retryAfterSeconds": { 297 | "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", 298 | "type": "integer", 299 | "format": "int32" 300 | }, 301 | "uid": { 302 | "description": "UID of the resource. (when there is a single resource which can be described). More info: http://kubernetes.io/docs/user-guide/identifiers#uids", 303 | "type": "string" 304 | } 305 | } 306 | }, 307 | "v1.UserInfo": { 308 | "description": "UserInfo holds the information about the user needed to implement the user.Info interface.", 309 | "type": "object", 310 | "properties": { 311 | "extra": { 312 | "description": "Any additional information provided by the authenticator.", 313 | "type": "object", 314 | "additionalProperties": { 315 | "type": "array", 316 | "items": { 317 | "type": "string" 318 | } 319 | } 320 | }, 321 | "groups": { 322 | "description": "The names of groups this user is a part of.", 323 | "type": "array", 324 | "items": { 325 | "type": "string" 326 | } 327 | }, 328 | "uid": { 329 | "description": "A unique value that identifies this user across time. If this user is deleted and another user by the same name is added, they will have different UIDs.", 330 | "type": "string" 331 | }, 332 | "username": { 333 | "description": "The name that uniquely identifies this user among all active users.", 334 | "type": "string" 335 | } 336 | } 337 | } 338 | }, 339 | "responses": {} 340 | } -------------------------------------------------------------------------------- /Kncs.CrdController/Crd/CSharpAppOperator.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Kncs.CrdController.OperatorSDK; 3 | using k8s; 4 | using k8s.Models; 5 | using Newtonsoft.Json; 6 | 7 | namespace Kncs.CrdController.Crd; 8 | 9 | public class CSharpAppOperator : IOperationHandler 10 | { 11 | private readonly string _watchedNamespace; 12 | private const string CSharpAppRunnerImage = "jijiechen/csharp-app-runner:202205211630"; 13 | private const string LastApplyConfigAnnoKey = "last-applied-configuration"; 14 | 15 | public CSharpAppOperator(string watchedNamespace) 16 | { 17 | _watchedNamespace = watchedNamespace; 18 | } 19 | 20 | public async Task CheckCurrentState(Kubernetes k8s) 21 | { 22 | var response = await k8s.ListNamespacedCustomObjectWithHttpMessagesAsync(CSharpApp.SchemaGroup, CSharpApp.SchemaVersion, 23 | _watchedNamespace, CSharpApp.SchemaKindPlural); 24 | 25 | if (!response.Response.IsSuccessStatusCode) 26 | { 27 | return; 28 | } 29 | 30 | var responseText = await response.Response.Content.ReadAsStringAsync(); 31 | var appListObj = JsonConvert.DeserializeObject(responseText); 32 | 33 | foreach (var item in appListObj!.Items!) 34 | { 35 | await OnUpdated(k8s, item); 36 | } 37 | } 38 | 39 | public async Task OnAdded(Kubernetes kubeClient, CSharpApp item) 40 | { 41 | if (item.Metadata == null || item.Spec == null) 42 | { 43 | return; 44 | } 45 | 46 | var lastApplied = item.Metadata?.Annotations?[LastApplyConfigAnnoKey]; 47 | if (lastApplied != null) 48 | { 49 | await OnUpdated(kubeClient, item); 50 | return; 51 | } 52 | 53 | var dummyLastCfg = new LastApplyConfiguration(); 54 | var ownerRef = BuildOwnerReference(item); 55 | 56 | var configmap = EnsureSourceCodeConfigMap(kubeClient, item, ownerRef); 57 | 58 | EnsurePods(kubeClient, item, dummyLastCfg, ownerRef, configmap); 59 | 60 | EnsureService(kubeClient, item, dummyLastCfg, ownerRef); 61 | 62 | var newlyAppliedConfig = new LastApplyConfiguration 63 | { 64 | CodeHash = GetCodeHash(item), 65 | Replicas = item.Spec!.Replicas, 66 | Service = item.Spec.Service 67 | }; 68 | var lastApplyCfgAnnoVal = JsonConvert.SerializeObject(newlyAppliedConfig); 69 | 70 | var appPatches = new List(); 71 | if (item.Metadata!.Annotations == null) 72 | { 73 | appPatches.Add(new 74 | { 75 | op = "add", 76 | path = $"/metadata/annotations", 77 | value = new Dictionary 78 | { 79 | { LastApplyConfigAnnoKey, lastApplyCfgAnnoVal } 80 | } 81 | }); 82 | 83 | } 84 | else 85 | { 86 | appPatches.Add(new 87 | { 88 | op = "add", 89 | path = $"/metadata/annotations/{LastApplyConfigAnnoKey}", 90 | value = lastApplyCfgAnnoVal 91 | }); 92 | } 93 | 94 | kubeClient.PatchNamespacedCustomObject( 95 | new V1Patch(JsonConvert.SerializeObject(appPatches), V1Patch.PatchType.JsonPatch), 96 | CSharpApp.SchemaGroup, 97 | CSharpApp.SchemaVersion, 98 | item.Metadata.NamespaceProperty, 99 | CSharpApp.SchemaKindPlural, 100 | item.Metadata.Name); 101 | } 102 | 103 | 104 | public Task OnDeleted(Kubernetes kubeClient, CSharpApp item) 105 | { 106 | CleanupService(kubeClient, item); 107 | 108 | CleanupPods(kubeClient, item, null /* delete all pods */); 109 | 110 | CleanupConfigMap(kubeClient, item); 111 | 112 | return Task.CompletedTask; 113 | } 114 | 115 | 116 | public async Task OnUpdated(Kubernetes kubeClient, CSharpApp item) 117 | { 118 | if (item?.Metadata == null || item.Spec == null) 119 | { 120 | return; 121 | } 122 | 123 | var ownerRef = BuildOwnerReference(item); 124 | 125 | LastApplyConfiguration? lastAppliedConfig = null; 126 | var lastApplied = item.Metadata.Annotations?[LastApplyConfigAnnoKey]; 127 | if (lastApplied != null) 128 | { 129 | lastAppliedConfig = JsonConvert.DeserializeObject(lastApplied); 130 | } 131 | 132 | if (lastAppliedConfig == null) 133 | { 134 | await OnDeleted(kubeClient, item); 135 | await OnAdded(kubeClient, item); 136 | return; 137 | } 138 | 139 | var codeConfigMap = EnsureSourceCodeConfigMap(kubeClient, item, ownerRef); 140 | 141 | EnsurePods(kubeClient, item, lastAppliedConfig, ownerRef, codeConfigMap); 142 | 143 | EnsureService(kubeClient, item, lastAppliedConfig, ownerRef); 144 | 145 | var codeHash = GetCodeHash(item); 146 | if (lastAppliedConfig.CodeHash == codeHash) 147 | { 148 | return; 149 | } 150 | 151 | var newlyAppliedConfig = new LastApplyConfiguration 152 | { 153 | CodeHash = codeHash, 154 | Replicas = item.Spec?.Replicas ?? 0, 155 | Service = item.Spec?.Service 156 | }; 157 | var appPatches = new List() 158 | { 159 | new 160 | { 161 | op = "replace", 162 | path = $"/metadata/annotations/{LastApplyConfigAnnoKey}", 163 | value = JsonConvert.SerializeObject(newlyAppliedConfig) 164 | } 165 | }; 166 | 167 | kubeClient.PatchNamespacedCustomObject(new V1Patch( JsonConvert.SerializeObject(appPatches), V1Patch.PatchType.JsonPatch), 168 | CSharpApp.SchemaGroup, 169 | CSharpApp.SchemaVersion, 170 | item.Metadata.NamespaceProperty, 171 | CSharpApp.SchemaKindPlural, 172 | item.Metadata.Name); 173 | } 174 | 175 | 176 | public Task OnBookmarked(Kubernetes k8s, CSharpApp crd) 177 | { 178 | return Task.CompletedTask; 179 | } 180 | 181 | public Task OnError(Kubernetes k8s, CSharpApp crd) 182 | { 183 | Console.Error.WriteLine("Some error happens..."); 184 | return Task.CompletedTask; 185 | } 186 | 187 | 188 | private static V1OwnerReference BuildOwnerReference(CSharpApp item) 189 | { 190 | var ownerRef = new V1OwnerReference() 191 | { 192 | Kind = item.Kind, 193 | ApiVersion = item.ApiVersion, 194 | Name = item.Metadata.Name, 195 | Uid = item.Metadata.Uid 196 | }; 197 | return ownerRef; 198 | } 199 | 200 | private void EnsurePods(Kubernetes kubeClient, CSharpApp item, LastApplyConfiguration lastAppliedConfig, 201 | V1OwnerReference ownerRef, string codeConfigMap) 202 | { 203 | var codeHash = GetCodeHash(item); 204 | var zeroPods = false; 205 | if (lastAppliedConfig.CodeHash != null && lastAppliedConfig.CodeHash != codeHash) 206 | { 207 | // 源代码已改变,清除旧的 Pod,等待下面的流程来创建 208 | CleanupPods(kubeClient, item, lastAppliedConfig.CodeHash); 209 | zeroPods = true; 210 | } 211 | 212 | var currentPods = Array.Empty(); 213 | if (!zeroPods) 214 | { 215 | 216 | currentPods = FindPods(kubeClient, item, codeHash) 217 | .Where(pod => pod.Metadata?.DeletionTimestamp == null).ToArray(); 218 | } 219 | var offset = item.Spec!.Replicas - currentPods.Length; 220 | if (offset > 0) 221 | { 222 | var podCreated = 0; 223 | do 224 | { 225 | CreatePod(kubeClient, item, codeHash!, ownerRef, codeConfigMap!); 226 | } while (++podCreated < offset); 227 | } 228 | else if (offset < 0) 229 | { 230 | // todo: deal with terminating! 231 | offset = -1 * offset; 232 | 233 | var podDeleted = 0; 234 | do 235 | { 236 | kubeClient.DeleteNamespacedPod(currentPods[podDeleted].Metadata.Name, item.Metadata.NamespaceProperty); 237 | } while (++podDeleted < offset); 238 | } 239 | } 240 | 241 | private void EnsureService(Kubernetes kubeClient, CSharpApp item, LastApplyConfiguration lastAppliedConfig, 242 | V1OwnerReference ownerRef) 243 | { 244 | if (item.Spec!.Service == null && lastAppliedConfig.Service != null) 245 | { 246 | CleanupService(kubeClient, item); 247 | } 248 | 249 | var existingSvc = FindService(kubeClient, item); 250 | if (item.Spec.Service != null && lastAppliedConfig.Service == null && existingSvc == null) 251 | { 252 | CreateService(kubeClient, item, ownerRef); 253 | } 254 | 255 | if (item.Spec.Service != null && lastAppliedConfig.Service != null 256 | && !item.Spec.Service.Equals(lastAppliedConfig.Service)) 257 | { 258 | if (existingSvc == null) 259 | { 260 | CreateService(kubeClient, item, ownerRef); 261 | } 262 | else 263 | { 264 | var patches = new List(); 265 | if (existingSvc.Spec.Type != item.Spec.Service.Type) 266 | { 267 | patches.Add(new 268 | { 269 | op = "replace", 270 | path = "/spec/type", 271 | value = item.Spec.Service.Type 272 | }); 273 | } 274 | 275 | existingSvc.Spec.Type = item.Spec.Service.Type; 276 | var svcPort = existingSvc.Spec.Ports.FirstOrDefault(p => p.Port == item.Spec.Service.Port); 277 | if (svcPort == null) 278 | { 279 | var ports = new List() 280 | { 281 | new() { Port = item.Spec.Service.Port, TargetPort = item.Spec.Service.Port } 282 | }; 283 | patches.Add(new 284 | { 285 | op = "replace", 286 | path = "/spec/ports", 287 | value = JsonConvert.SerializeObject(ports) 288 | }); 289 | } 290 | 291 | kubeClient.PatchNamespacedService(new V1Patch(JsonConvert.SerializeObject(patches), V1Patch.PatchType.JsonPatch), existingSvc.Metadata.Name, 292 | existingSvc.Metadata.NamespaceProperty); 293 | } 294 | } 295 | } 296 | 297 | private string EnsureSourceCodeConfigMap(Kubernetes kubeClient, CSharpApp item, V1OwnerReference ownerRef) 298 | { 299 | if (item == null) throw new ArgumentNullException(nameof(item)); 300 | 301 | var desiredSourceCode = (item.Spec?.Code ?? "").Trim(); 302 | var existingConfigMap = FindConfigMap(kubeClient, item); 303 | if (existingConfigMap != null) 304 | { 305 | var hasData = existingConfigMap.Data != null; 306 | var patches = new List(); 307 | if (!hasData) 308 | { 309 | patches.Add(new 310 | { 311 | op = "add", 312 | path = "/data", 313 | value = new Dictionary 314 | { 315 | { "Program.cs", desiredSourceCode } 316 | } 317 | }); 318 | } 319 | else if (!existingConfigMap.Data!.ContainsKey("Program.cs")) 320 | { 321 | patches.Add( 322 | new 323 | { 324 | op = "add", 325 | path = "/data/Program.cs", 326 | value = desiredSourceCode 327 | }); 328 | } 329 | else 330 | { 331 | var source = existingConfigMap.Data["Program.cs"]; 332 | if (source == null || source.Trim() != desiredSourceCode) 333 | { 334 | patches.Add( 335 | new 336 | { 337 | op = "replace", 338 | path = "/data/Program.cs", 339 | value = desiredSourceCode 340 | }); 341 | } 342 | } 343 | 344 | if (patches.Count > 0) 345 | { 346 | var patch = new V1Patch(JsonConvert.SerializeObject(patches), V1Patch.PatchType.JsonPatch); 347 | kubeClient.PatchNamespacedConfigMap(patch, existingConfigMap.Metadata.Name, item.Metadata.NamespaceProperty); 348 | } 349 | 350 | return existingConfigMap.Metadata.Name; 351 | } 352 | 353 | 354 | var cmName = $"csapp-src-{item.Metadata.Name}"; 355 | var configMap = new V1ConfigMap 356 | { 357 | ApiVersion = "v1", 358 | Kind = "ConfigMap", 359 | Data = new Dictionary 360 | { 361 | { "Program.cs", desiredSourceCode } 362 | }, 363 | Metadata = new V1ObjectMeta() 364 | { 365 | Labels = new Dictionary() 366 | } 367 | }; 368 | configMap.AddOwnerReference(ownerRef); 369 | 370 | configMap.Metadata.Name = cmName; 371 | configMap.Metadata.Labels["csharpapp"] = item.Metadata.Name; 372 | kubeClient.CreateNamespacedConfigMap(configMap, item.Metadata.NamespaceProperty); 373 | return cmName; 374 | } 375 | 376 | #region primitive operations 377 | 378 | void CreatePod(Kubernetes kubeClient, CSharpApp item, string codeHash, V1OwnerReference ownerRef, string configmap) 379 | { 380 | var shortHash = codeHash.Substring(0, 6); 381 | var podName = $"csa-{item.Metadata.Name}-{shortHash}-{GenerateRandomName()}"; 382 | var pod = new V1Pod 383 | { 384 | ApiVersion = "v1", 385 | Kind = "Pod", 386 | Spec = new V1PodSpec() { Containers = new List(), Volumes = new List() }, 387 | Metadata = new V1ObjectMeta() 388 | { 389 | Labels = new Dictionary() 390 | } 391 | }; 392 | 393 | pod.Metadata.Name = podName; 394 | pod.Metadata.Labels["csharpapp"] = item.Metadata.Name; 395 | pod.Metadata.Labels["codehash"] = codeHash; 396 | pod.AddOwnerReference(ownerRef); 397 | 398 | var cmVolume = new V1Volume("cs-source"); 399 | cmVolume.ConfigMap = new V1ConfigMapVolumeSource() 400 | { 401 | Name = configmap 402 | }; 403 | pod.Spec.Volumes.Add(cmVolume); 404 | 405 | var container = new V1Container("csa") 406 | { 407 | Image = GetRunnerImage(), 408 | ImagePullPolicy = "IfNotPresent", 409 | VolumeMounts = new List(), 410 | }; 411 | container.VolumeMounts.Add(new V1VolumeMount("/etc/cs-source", "cs-source")); 412 | pod.Spec.Containers.Add(container); 413 | 414 | kubeClient.CreateNamespacedPod(pod, item.Metadata.NamespaceProperty); 415 | } 416 | 417 | private void CreateService(Kubernetes kubeClient, CSharpApp item, V1OwnerReference ownerRef) 418 | { 419 | var svc = new V1Service() 420 | { 421 | ApiVersion = "v1", 422 | Kind = "Service", 423 | Spec = new V1ServiceSpec() 424 | { 425 | Ports = new List() 426 | }, 427 | Metadata = new V1ObjectMeta() 428 | { 429 | Labels = new Dictionary() 430 | } 431 | }; 432 | 433 | svc.Metadata.Name = item.Metadata.Name; 434 | svc.Metadata.Labels["csharpapp"] = item.Metadata.Name; 435 | svc.Spec.Type = item.Spec!.Service!.Type; 436 | svc.Spec.Ports.Add(new V1ServicePort() 437 | { 438 | Port = item.Spec.Service.Port, 439 | TargetPort = item.Spec.Service.Port 440 | }); 441 | svc.Spec.Selector = new Dictionary() 442 | { 443 | { "csharpapp", item.Metadata.Name } 444 | }; 445 | svc.AddOwnerReference(ownerRef); 446 | 447 | kubeClient.CreateNamespacedService(svc, item.Metadata.NamespaceProperty); 448 | } 449 | 450 | private void CleanupService(Kubernetes kubeClient, CSharpApp item) 451 | { 452 | var svc = FindService(kubeClient, item); 453 | if (svc != null) 454 | { 455 | kubeClient.DeleteNamespacedService(svc.Name(), svc.Namespace()); 456 | } 457 | } 458 | 459 | private void CleanupPods(Kubernetes kubeClient, CSharpApp item, string? lastCodeHash) 460 | { 461 | var pods = FindPods(kubeClient, item, lastCodeHash).ToArray(); 462 | 463 | foreach (var pod in pods) 464 | { 465 | kubeClient.DeleteNamespacedPod(pod.Name(), pod.Namespace()); 466 | } 467 | } 468 | 469 | private void CleanupConfigMap(Kubernetes kubeClient, CSharpApp item) 470 | { 471 | var targetConfigMap = FindConfigMap(kubeClient, item); 472 | 473 | if (targetConfigMap != null) 474 | { 475 | kubeClient.DeleteNamespacedConfigMap(targetConfigMap.Name(), targetConfigMap.Namespace()); 476 | } 477 | } 478 | 479 | private V1ConfigMap? FindConfigMap(Kubernetes kubeClient, CSharpApp item) 480 | { 481 | var labelSelector = $"csharpapp={item.Metadata.Name}"; 482 | var configMaps = kubeClient.ListNamespacedConfigMap(item.Metadata.NamespaceProperty, 483 | false, null, null, labelSelector); 484 | 485 | var targetConfigMap = configMaps.Items.FirstOrDefault(cm => cm.OwnerReferences().Any(o 486 | => o.ApiVersion == item.ApiVersion && o.Kind == item.Kind && o.Name == item.Metadata.Name)); 487 | return targetConfigMap; 488 | } 489 | 490 | private V1Service? FindService(Kubernetes kubeClient, CSharpApp item) 491 | { 492 | var labelSelector = $"csharpapp={item.Metadata.Name}"; 493 | var svcList = kubeClient.ListNamespacedService(item.Metadata.NamespaceProperty, 494 | false, null, null, labelSelector); 495 | 496 | var targetSvc = svcList.Items.FirstOrDefault(cm => cm.OwnerReferences().Any(o 497 | => o.ApiVersion == item.ApiVersion && o.Kind == item.Kind && o.Name == item.Metadata.Name)); 498 | return targetSvc; 499 | } 500 | 501 | private IEnumerable FindPods(Kubernetes kubeClient, CSharpApp item, string? codeHash) 502 | { 503 | var labelSelector = $"csharpapp={item.Metadata.Name}"; 504 | if (codeHash != null) 505 | { 506 | labelSelector += $",codehash={codeHash}"; 507 | } 508 | 509 | var pods = kubeClient.ListNamespacedPod(item.Metadata.NamespaceProperty, 510 | false, null, null, labelSelector); 511 | 512 | return pods.Items.Where(cm => cm.OwnerReferences().Any(o 513 | => o.ApiVersion == item.ApiVersion && o.Kind == item.Kind && o.Name == item.Metadata.Name)); 514 | } 515 | 516 | #endregion 517 | 518 | #region helper methods 519 | 520 | static readonly Random NameGeneratorRandom = new(); 521 | private const byte RandomNameLength = 4; 522 | 523 | static string? GetCodeHash(CSharpApp item) 524 | { 525 | var code = item.Spec?.Code; 526 | if (string.IsNullOrEmpty(code)) 527 | { 528 | return null; 529 | } 530 | 531 | var hashCode = GetDeterministicHashCode(code); 532 | return hashCode.ToString("D").Replace('-', 'u'); 533 | } 534 | 535 | static int GetDeterministicHashCode(string str) 536 | { 537 | unchecked 538 | { 539 | int hash1 = (5381 << 16) + 5381; 540 | int hash2 = hash1; 541 | 542 | for (int i = 0; i < str.Length; i += 2) 543 | { 544 | hash1 = ((hash1 << 5) + hash1) ^ str[i]; 545 | if (i == str.Length - 1) 546 | break; 547 | hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; 548 | } 549 | 550 | return hash1 + (hash2 * 1566083941); 551 | } 552 | } 553 | 554 | static string GenerateRandomName() 555 | { 556 | var stringBuilder = new StringBuilder(); 557 | while (stringBuilder.Length < RandomNameLength) 558 | { 559 | var letter = NameGeneratorRandom.Next(97, 122); 560 | stringBuilder.Append((char)letter); 561 | } 562 | 563 | return stringBuilder.ToString(); 564 | } 565 | 566 | static string GetRunnerImage() 567 | { 568 | var customizedRunner = Environment.GetEnvironmentVariable("RUNNER_IMAGE"); 569 | return !string.IsNullOrEmpty(customizedRunner) ? customizedRunner : CSharpAppRunnerImage; 570 | } 571 | 572 | #endregion 573 | } 574 | -------------------------------------------------------------------------------- /Kncs.Webhook/AdminReviewModels/AdmissionReview.cs: -------------------------------------------------------------------------------- 1 | //---------------------- 2 | // 3 | // Generated using the NSwag toolchain v13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) 4 | // 5 | //---------------------- 6 | 7 | #pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." 8 | #pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." 9 | #pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' 10 | #pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... 11 | #pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." 12 | #pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" 13 | #pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" 14 | #pragma warning disable 8603 // Disable "CS8603 Possible null reference return" 15 | 16 | namespace Kncs.Webhook 17 | { 18 | using System = global::System; 19 | 20 | 21 | 22 | /// 23 | /// RawExtension is used to hold extensions in external versions. 24 | ///
25 | ///
To use this, make a field which has RawExtension as its type in your external, versioned struct, and Object in your internal struct. You also need to register your various plugin types. 26 | ///
27 | ///
// Internal package: type MyAPIObject struct { 28 | ///
runtime.TypeMeta `json:",inline"` 29 | ///
MyPlugin runtime.Object `json:"myPlugin"` 30 | ///
} type PluginA struct { 31 | ///
AOption string `json:"aOption"` 32 | ///
} 33 | ///
34 | ///
// External package: type MyAPIObject struct { 35 | ///
runtime.TypeMeta `json:",inline"` 36 | ///
MyPlugin runtime.RawExtension `json:"myPlugin"` 37 | ///
} type PluginA struct { 38 | ///
AOption string `json:"aOption"` 39 | ///
} 40 | ///
41 | ///
// On the wire, the JSON will look something like this: { 42 | ///
"kind":"MyAPIObject", 43 | ///
"apiVersion":"v1", 44 | ///
"myPlugin": { 45 | ///
"kind":"PluginA", 46 | ///
"aOption":"foo", 47 | ///
}, 48 | ///
} 49 | ///
50 | ///
So what happens? Decode first uses json or yaml to unmarshal the serialized data into your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. The next step is to copy (using pkg/conversion) into the internal struct. The runtime package's DefaultScheme has conversion functions installed which will unpack the JSON stored in RawExtension, turning it into the correct object type, and storing it in the Object. (TODO: In the case where the object is of an unknown type, a runtime.Unknown object will be created and stored.) 51 | ///
52 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 53 | public partial class RawExtension 54 | { 55 | 56 | } 57 | 58 | /// 59 | /// AdmissionRequest describes the admission.Attributes for the admission request. 60 | /// 61 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 62 | public partial class AdmissionRequest 63 | { 64 | /// 65 | /// DryRun indicates that modifications will definitely not be persisted for this request. Defaults to false. 66 | /// 67 | [System.Text.Json.Serialization.JsonPropertyName("dryRun")] 68 | public bool? DryRun { get; set; } 69 | 70 | /// 71 | /// Kind is the fully-qualified type of object being submitted (for example, v1.Pod or autoscaling.v1.Scale) 72 | /// 73 | [System.Text.Json.Serialization.JsonPropertyName("kind")] 74 | [System.ComponentModel.DataAnnotations.Required] 75 | public GroupVersionKind Kind { get; set; } = new GroupVersionKind(); 76 | 77 | /// 78 | /// Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and rely on the server to generate the name. If that is the case, this field will contain an empty string. 79 | /// 80 | [System.Text.Json.Serialization.JsonPropertyName("name")] 81 | public string Name { get; set; } 82 | 83 | /// 84 | /// Namespace is the namespace associated with the request (if any). 85 | /// 86 | [System.Text.Json.Serialization.JsonPropertyName("namespace")] 87 | public string Namespace { get; set; } 88 | 89 | /// 90 | /// Object is the object from the incoming request. 91 | /// 92 | [System.Text.Json.Serialization.JsonPropertyName("object")] 93 | public System.Text.Json.JsonElement Object { get; set; } 94 | 95 | /// 96 | /// OldObject is the existing object. Only populated for DELETE and UPDATE requests. 97 | /// 98 | [System.Text.Json.Serialization.JsonPropertyName("oldObject")] 99 | public System.Text.Json.JsonElement OldObject { get; set; } 100 | 101 | /// 102 | /// Operation is the operation being performed. This may be different than the operation requested. e.g. a patch can result in either a CREATE or UPDATE Operation. 103 | /// 104 | [System.Text.Json.Serialization.JsonPropertyName("operation")] 105 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 106 | public string Operation { get; set; } 107 | 108 | /// 109 | /// Options is the operation option structure of the operation being performed. e.g. `meta.k8s.io/v1.DeleteOptions` or `meta.k8s.io/v1.CreateOptions`. This may be different than the options the caller provided. e.g. for a patch request the performed Operation might be a CREATE, in which case the Options will a `meta.k8s.io/v1.CreateOptions` even though the caller provided `meta.k8s.io/v1.PatchOptions`. 110 | /// 111 | [System.Text.Json.Serialization.JsonPropertyName("options")] 112 | public System.Text.Json.JsonElement Options { get; set; } 113 | 114 | /// 115 | /// RequestKind is the fully-qualified type of the original API request (for example, v1.Pod or autoscaling.v1.Scale). If this is specified and differs from the value in "kind", an equivalent match and conversion was performed. 116 | ///
117 | ///
For example, if deployments can be modified via apps/v1 and apps/v1beta1, and a webhook registered a rule of `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]` and `matchPolicy: Equivalent`, an API request to apps/v1beta1 deployments would be converted and sent to the webhook with `kind: {group:"apps", version:"v1", kind:"Deployment"}` (matching the rule the webhook registered for), and `requestKind: {group:"apps", version:"v1beta1", kind:"Deployment"}` (indicating the kind of the original API request). 118 | ///
119 | ///
See documentation for the "matchPolicy" field in the webhook configuration type for more details. 120 | ///
121 | [System.Text.Json.Serialization.JsonPropertyName("requestKind")] 122 | public GroupVersionKind RequestKind { get; set; } 123 | 124 | /// 125 | /// RequestResource is the fully-qualified resource of the original API request (for example, v1.pods). If this is specified and differs from the value in "resource", an equivalent match and conversion was performed. 126 | ///
127 | ///
For example, if deployments can be modified via apps/v1 and apps/v1beta1, and a webhook registered a rule of `apiGroups:["apps"], apiVersions:["v1"], resources: ["deployments"]` and `matchPolicy: Equivalent`, an API request to apps/v1beta1 deployments would be converted and sent to the webhook with `resource: {group:"apps", version:"v1", resource:"deployments"}` (matching the resource the webhook registered for), and `requestResource: {group:"apps", version:"v1beta1", resource:"deployments"}` (indicating the resource of the original API request). 128 | ///
129 | ///
See documentation for the "matchPolicy" field in the webhook configuration type. 130 | ///
131 | [System.Text.Json.Serialization.JsonPropertyName("requestResource")] 132 | public GroupVersionResource RequestResource { get; set; } 133 | 134 | /// 135 | /// RequestSubResource is the name of the subresource of the original API request, if any (for example, "status" or "scale") If this is specified and differs from the value in "subResource", an equivalent match and conversion was performed. See documentation for the "matchPolicy" field in the webhook configuration type. 136 | /// 137 | [System.Text.Json.Serialization.JsonPropertyName("requestSubResource")] 138 | public string RequestSubResource { get; set; } 139 | 140 | /// 141 | /// Resource is the fully-qualified resource being requested (for example, v1.pods) 142 | /// 143 | [System.Text.Json.Serialization.JsonPropertyName("resource")] 144 | [System.ComponentModel.DataAnnotations.Required] 145 | public GroupVersionResource Resource { get; set; } = new GroupVersionResource(); 146 | 147 | /// 148 | /// SubResource is the subresource being requested, if any (for example, "status" or "scale") 149 | /// 150 | [System.Text.Json.Serialization.JsonPropertyName("subResource")] 151 | public string SubResource { get; set; } 152 | 153 | /// 154 | /// UID is an identifier for the individual request/response. It allows us to distinguish instances of requests which are otherwise identical (parallel requests, requests when earlier requests did not modify etc) The UID is meant to track the round trip (request/response) between the KAS and the WebHook, not the user request. It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging. 155 | /// 156 | [System.Text.Json.Serialization.JsonPropertyName("uid")] 157 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 158 | public string Uid { get; set; } 159 | 160 | /// 161 | /// UserInfo is information about the requesting user 162 | /// 163 | [System.Text.Json.Serialization.JsonPropertyName("userInfo")] 164 | [System.ComponentModel.DataAnnotations.Required] 165 | public UserInfo UserInfo { get; set; } = new UserInfo(); 166 | 167 | } 168 | 169 | /// 170 | /// AdmissionResponse describes an admission response. 171 | /// 172 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 173 | public partial class AdmissionResponse 174 | { 175 | /// 176 | /// Allowed indicates whether or not the admission request was permitted. 177 | /// 178 | [System.Text.Json.Serialization.JsonPropertyName("allowed")] 179 | public bool Allowed { get; set; } 180 | 181 | /// 182 | /// AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted). MutatingAdmissionWebhook and ValidatingAdmissionWebhook admission controller will prefix the keys with admission webhook name (e.g. imagepolicy.example.com/error=image-blacklisted). AuditAnnotations will be provided by the admission webhook to add additional context to the audit log for this request. 183 | /// 184 | [System.Text.Json.Serialization.JsonPropertyName("auditAnnotations")] 185 | public System.Collections.Generic.IDictionary AuditAnnotations { get; set; } 186 | 187 | /// 188 | /// The patch body. Currently we only support "JSONPatch" which implements RFC 6902. 189 | /// 190 | [System.Text.Json.Serialization.JsonPropertyName("patch")] 191 | public byte[] Patch { get; set; } 192 | 193 | /// 194 | /// The type of Patch. Currently we only allow "JSONPatch". 195 | /// 196 | [System.Text.Json.Serialization.JsonPropertyName("patchType")] 197 | public string PatchType { get; set; } 198 | 199 | /// 200 | /// Result contains extra details into why an admission request was denied. This field IS NOT consulted in any way if "Allowed" is "true". 201 | /// 202 | [System.Text.Json.Serialization.JsonPropertyName("status")] 203 | public Status Status { get; set; } 204 | 205 | /// 206 | /// UID is an identifier for the individual request/response. This must be copied over from the corresponding AdmissionRequest. 207 | /// 208 | [System.Text.Json.Serialization.JsonPropertyName("uid")] 209 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 210 | public string Uid { get; set; } 211 | 212 | /// 213 | /// warnings is a list of warning messages to return to the requesting API client. Warning messages describe a problem the client making the API request should correct or be aware of. Limit warnings to 120 characters if possible. Warnings over 256 characters and large numbers of warnings may be truncated. 214 | /// 215 | [System.Text.Json.Serialization.JsonPropertyName("warnings")] 216 | public System.Collections.Generic.ICollection Warnings { get; set; } 217 | 218 | } 219 | 220 | /// 221 | /// AdmissionReview describes an admission review request/response. 222 | /// 223 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 224 | public partial class AdmissionReview 225 | { 226 | /// 227 | /// APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 228 | /// 229 | [System.Text.Json.Serialization.JsonPropertyName("apiVersion")] 230 | public string ApiVersion { get; set; } 231 | 232 | /// 233 | /// Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 234 | /// 235 | [System.Text.Json.Serialization.JsonPropertyName("kind")] 236 | public string Kind { get; set; } 237 | 238 | /// 239 | /// Request describes the attributes for the admission request. 240 | /// 241 | [System.Text.Json.Serialization.JsonPropertyName("request")] 242 | public AdmissionRequest Request { get; set; } 243 | 244 | /// 245 | /// Response describes the attributes for the admission response. 246 | /// 247 | [System.Text.Json.Serialization.JsonPropertyName("response")] 248 | public AdmissionResponse Response { get; set; } 249 | 250 | } 251 | 252 | /// 253 | /// GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling 254 | /// 255 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 256 | public partial class GroupVersionKind 257 | { 258 | [System.Text.Json.Serialization.JsonPropertyName("group")] 259 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 260 | public string Group { get; set; } 261 | 262 | [System.Text.Json.Serialization.JsonPropertyName("kind")] 263 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 264 | public string Kind { get; set; } 265 | 266 | [System.Text.Json.Serialization.JsonPropertyName("version")] 267 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 268 | public string Version { get; set; } 269 | 270 | } 271 | 272 | /// 273 | /// GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion to avoid automatic coersion. It doesn't use a GroupVersion to avoid custom marshalling 274 | /// 275 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 276 | public partial class GroupVersionResource 277 | { 278 | [System.Text.Json.Serialization.JsonPropertyName("group")] 279 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 280 | public string Group { get; set; } 281 | 282 | [System.Text.Json.Serialization.JsonPropertyName("resource")] 283 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 284 | public string Resource { get; set; } 285 | 286 | [System.Text.Json.Serialization.JsonPropertyName("version")] 287 | [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] 288 | public string Version { get; set; } 289 | 290 | } 291 | 292 | /// 293 | /// ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}. 294 | /// 295 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 296 | public partial class ListMeta 297 | { 298 | /// 299 | /// continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. 300 | /// 301 | [System.Text.Json.Serialization.JsonPropertyName("continue")] 302 | public string Continue { get; set; } 303 | 304 | /// 305 | /// remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. 306 | /// 307 | [System.Text.Json.Serialization.JsonPropertyName("remainingItemCount")] 308 | public long? RemainingItemCount { get; set; } 309 | 310 | /// 311 | /// String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency 312 | /// 313 | [System.Text.Json.Serialization.JsonPropertyName("resourceVersion")] 314 | public string ResourceVersion { get; set; } 315 | 316 | /// 317 | /// selfLink is a URL representing this object. Populated by the system. Read-only. 318 | ///
319 | ///
DEPRECATED Kubernetes will stop propagating this field in 1.20 release and the field is planned to be removed in 1.21 release. 320 | ///
321 | [System.Text.Json.Serialization.JsonPropertyName("selfLink")] 322 | public string SelfLink { get; set; } 323 | 324 | } 325 | 326 | /// 327 | /// Status is a return value for calls that don't return other objects. 328 | /// 329 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 330 | public partial class Status 331 | { 332 | /// 333 | /// APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 334 | /// 335 | [System.Text.Json.Serialization.JsonPropertyName("apiVersion")] 336 | public string ApiVersion { get; set; } 337 | 338 | /// 339 | /// Suggested HTTP return code for this status, 0 if not set. 340 | /// 341 | [System.Text.Json.Serialization.JsonPropertyName("code")] 342 | public int? Code { get; set; } 343 | 344 | /// 345 | /// Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type. 346 | /// 347 | [System.Text.Json.Serialization.JsonPropertyName("details")] 348 | public StatusDetails Details { get; set; } 349 | 350 | /// 351 | /// Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 352 | /// 353 | [System.Text.Json.Serialization.JsonPropertyName("kind")] 354 | public string Kind { get; set; } 355 | 356 | /// 357 | /// A human-readable description of the status of this operation. 358 | /// 359 | [System.Text.Json.Serialization.JsonPropertyName("message")] 360 | public string Message { get; set; } 361 | 362 | /// 363 | /// Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 364 | /// 365 | [System.Text.Json.Serialization.JsonPropertyName("metadata")] 366 | public ListMeta Metadata { get; set; } 367 | 368 | /// 369 | /// A machine-readable description of why this operation is in the "Failure" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. 370 | /// 371 | [System.Text.Json.Serialization.JsonPropertyName("reason")] 372 | public string Reason { get; set; } 373 | 374 | /// 375 | /// Status of the operation. One of: "Success" or "Failure". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status 376 | /// 377 | [System.Text.Json.Serialization.JsonPropertyName("status")] 378 | public string Status1 { get; set; } 379 | 380 | } 381 | 382 | /// 383 | /// StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered. 384 | /// 385 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 386 | public partial class StatusCause 387 | { 388 | /// 389 | /// The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. 390 | ///
391 | ///
Examples: 392 | ///
"name" - the field "name" on the current resource 393 | ///
"items[0].name" - the field "name" on the first array entry in "items" 394 | ///
395 | [System.Text.Json.Serialization.JsonPropertyName("field")] 396 | public string Field { get; set; } 397 | 398 | /// 399 | /// A human-readable description of the cause of the error. This field may be presented as-is to a reader. 400 | /// 401 | [System.Text.Json.Serialization.JsonPropertyName("message")] 402 | public string Message { get; set; } 403 | 404 | /// 405 | /// A machine-readable description of the cause of the error. If this value is empty there is no information available. 406 | /// 407 | [System.Text.Json.Serialization.JsonPropertyName("reason")] 408 | public string Reason { get; set; } 409 | 410 | } 411 | 412 | /// 413 | /// StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined. 414 | /// 415 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 416 | public partial class StatusDetails 417 | { 418 | /// 419 | /// The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. 420 | /// 421 | [System.Text.Json.Serialization.JsonPropertyName("causes")] 422 | public System.Collections.Generic.ICollection Causes { get; set; } 423 | 424 | /// 425 | /// The group attribute of the resource associated with the status StatusReason. 426 | /// 427 | [System.Text.Json.Serialization.JsonPropertyName("group")] 428 | public string Group { get; set; } 429 | 430 | /// 431 | /// The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 432 | /// 433 | [System.Text.Json.Serialization.JsonPropertyName("kind")] 434 | public string Kind { get; set; } 435 | 436 | /// 437 | /// The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). 438 | /// 439 | [System.Text.Json.Serialization.JsonPropertyName("name")] 440 | public string Name { get; set; } 441 | 442 | /// 443 | /// If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. 444 | /// 445 | [System.Text.Json.Serialization.JsonPropertyName("retryAfterSeconds")] 446 | public int? RetryAfterSeconds { get; set; } 447 | 448 | /// 449 | /// UID of the resource. (when there is a single resource which can be described). More info: http://kubernetes.io/docs/user-guide/identifiers#uids 450 | /// 451 | [System.Text.Json.Serialization.JsonPropertyName("uid")] 452 | public string Uid { get; set; } 453 | 454 | } 455 | 456 | /// 457 | /// UserInfo holds the information about the user needed to implement the user.Info interface. 458 | /// 459 | [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v13.0.0.0))")] 460 | public partial class UserInfo 461 | { 462 | /// 463 | /// Any additional information provided by the authenticator. 464 | /// 465 | [System.Text.Json.Serialization.JsonPropertyName("extra")] 466 | public System.Collections.Generic.IDictionary> Extra { get; set; } 467 | 468 | /// 469 | /// The names of groups this user is a part of. 470 | /// 471 | [System.Text.Json.Serialization.JsonPropertyName("groups")] 472 | public System.Collections.Generic.ICollection Groups { get; set; } 473 | 474 | /// 475 | /// A unique value that identifies this user across time. If this user is deleted and another user by the same name is added, they will have different UIDs. 476 | /// 477 | [System.Text.Json.Serialization.JsonPropertyName("uid")] 478 | public string Uid { get; set; } 479 | 480 | /// 481 | /// The name that uniquely identifies this user among all active users. 482 | /// 483 | [System.Text.Json.Serialization.JsonPropertyName("username")] 484 | public string Username { get; set; } 485 | 486 | } 487 | 488 | 489 | 490 | } 491 | 492 | #pragma warning restore 1591 493 | #pragma warning restore 1573 494 | #pragma warning restore 472 495 | #pragma warning restore 114 496 | #pragma warning restore 108 497 | #pragma warning restore 3016 498 | #pragma warning restore 8603 --------------------------------------------------------------------------------