├── samples
├── basic
│ ├── .dockerignore
│ ├── deploy
│ │ ├── service_account.yaml
│ │ ├── crds
│ │ │ ├── cr.yaml
│ │ │ └── crd.yaml
│ │ ├── role_binding.yaml
│ │ ├── operator.yaml
│ │ └── role.yaml
│ ├── Dockerfile
│ ├── k8s.Operators.Samples.Basic.csproj
│ ├── MyResource.cs
│ ├── MyResourceController.cs
│ ├── README.md
│ └── Program.cs
└── dynamic
│ ├── .dockerignore
│ ├── deploy
│ ├── service_account.yaml
│ ├── crds
│ │ ├── cr.yaml
│ │ └── crd.yaml
│ ├── role_binding.yaml
│ ├── operator.yaml
│ └── role.yaml
│ ├── Dockerfile
│ ├── MyDynamicResource.cs
│ ├── k8s.Operators.Samples.Dynamic.csproj
│ ├── README.md
│ ├── MyDynamicResourceController.cs
│ └── Program.cs
├── docs
└── writing-csharp-operator.md
├── src
└── k8s.Operators
│ ├── k8s.Operators.snk
│ ├── Models
│ ├── IStatus.cs
│ ├── DynamicCustomResource.cs
│ ├── CustomResourceList.cs
│ ├── RetryPolicy.cs
│ ├── CustomResourceDefinitionAttribute.cs
│ ├── CustomResourceEvent.cs
│ ├── Disposable.cs
│ ├── OperatorConfiguration.cs
│ └── CustomResource.cs
│ ├── Logging
│ ├── SilentLogger.cs
│ └── ConsoleTracingInterceptor.cs
│ ├── IController.cs
│ ├── k8s.Operators.csproj
│ ├── IOperator.cs
│ ├── EventWatcher.cs
│ ├── ResourceChangeTracker.cs
│ ├── EventManager.cs
│ ├── Operator.cs
│ └── Controller.cs
├── tests
└── k8s.Operators.Tests
│ ├── TestableDynamicController.cs
│ ├── TestableDynamicCustomResource.cs
│ ├── TestableCustomResource.cs
│ ├── k8s.Operators.Tests.csproj
│ ├── TestableOperator.cs
│ ├── TestableController.cs
│ ├── BaseTests.cs
│ ├── OperatorTests.cs
│ └── ControllerTests.cs
├── .vscode
├── launch.json
└── tasks.json
├── .github
└── workflows
│ └── dotnet-core.yml
├── README.md
├── csharp-operator-sdk.sln
├── .gitignore
└── LICENSE
/samples/basic/.dockerignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
--------------------------------------------------------------------------------
/samples/dynamic/.dockerignore:
--------------------------------------------------------------------------------
1 | bin/
2 | obj/
--------------------------------------------------------------------------------
/docs/writing-csharp-operator.md:
--------------------------------------------------------------------------------
1 | # Writing a Kubernetes Operator in C#
2 |
3 | TODO
--------------------------------------------------------------------------------
/samples/basic/deploy/service_account.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ServiceAccount
3 | metadata:
4 | name: basic-operator
5 |
--------------------------------------------------------------------------------
/samples/dynamic/deploy/service_account.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ServiceAccount
3 | metadata:
4 | name: dynamic-operator
5 |
--------------------------------------------------------------------------------
/src/k8s.Operators/k8s.Operators.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/falox/csharp-operator-sdk/HEAD/src/k8s.Operators/k8s.Operators.snk
--------------------------------------------------------------------------------
/samples/basic/deploy/crds/cr.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: "csharp-operator.example.com/v1"
2 | kind: MyResource
3 | metadata:
4 | name: mr1
5 | spec:
6 | desiredProperty: 1
--------------------------------------------------------------------------------
/samples/dynamic/deploy/crds/cr.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: "csharp-operator.example.com/v1"
2 | kind: MyResource
3 | metadata:
4 | name: mr1
5 | spec:
6 | desiredProperty: 1
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/IStatus.cs:
--------------------------------------------------------------------------------
1 | namespace k8s.Operators
2 | {
3 | ///
4 | /// Kubernetes custom resource that exposes status
5 | ///
6 | public interface IStatus
7 | {
8 | object Status { get; set; }
9 | }
10 | }
--------------------------------------------------------------------------------
/samples/basic/deploy/role_binding.yaml:
--------------------------------------------------------------------------------
1 | kind: RoleBinding
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | metadata:
4 | name: basic-operator
5 | subjects:
6 | - kind: ServiceAccount
7 | name: basic-operator
8 | roleRef:
9 | kind: Role
10 | name: basic-operator
11 | apiGroup: rbac.authorization.k8s.io
12 |
--------------------------------------------------------------------------------
/samples/dynamic/deploy/role_binding.yaml:
--------------------------------------------------------------------------------
1 | kind: RoleBinding
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | metadata:
4 | name: dynamic-operator
5 | subjects:
6 | - kind: ServiceAccount
7 | name: dynamic-operator
8 | roleRef:
9 | kind: Role
10 | name: dynamic-operator
11 | apiGroup: rbac.authorization.k8s.io
12 |
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/DynamicCustomResource.cs:
--------------------------------------------------------------------------------
1 | using System.Dynamic;
2 |
3 | namespace k8s.Operators
4 | {
5 | ///
6 | /// Represents a Kubernetes custom resource with dynamic typed spec and status
7 | ///
8 | public abstract class DynamicCustomResource : CustomResource
9 | {
10 | }
11 | }
--------------------------------------------------------------------------------
/samples/basic/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
2 | WORKDIR /app
3 | COPY . ./
4 |
5 | RUN dotnet restore
6 | RUN dotnet publish samples/basic/k8s.Operators.Samples.Basic.csproj -c Release -o out
7 |
8 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
9 | WORKDIR /app
10 | COPY --from=build-env /app/out .
11 | ENTRYPOINT ["dotnet", "k8s.Operators.Samples.Basic.dll", "--debug"]
12 |
--------------------------------------------------------------------------------
/samples/dynamic/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
2 | WORKDIR /app
3 | COPY . ./
4 |
5 | RUN dotnet restore
6 | RUN dotnet publish samples/dynamic/k8s.Operators.Samples.Dynamic.csproj -c Release -o out
7 |
8 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
9 | WORKDIR /app
10 | COPY --from=build-env /app/out .
11 | ENTRYPOINT ["dotnet", "k8s.Operators.Samples.Dynamic.dll", "--debug"]
12 |
--------------------------------------------------------------------------------
/samples/dynamic/MyDynamicResource.cs:
--------------------------------------------------------------------------------
1 | using k8s.Operators;
2 | using Newtonsoft.Json;
3 | using System.Collections.Generic;
4 |
5 | namespace k8s.Operators.Samples.Dynamic
6 | {
7 | [CustomResourceDefinition("csharp-operator.example.com", "v1", "myresources")]
8 | public class MyDynamicResource : DynamicCustomResource
9 | {
10 | public override string ToString()
11 | {
12 | return $"{Metadata.NamespaceProperty}/{Metadata.Name} (gen: {Metadata.Generation}), Spec: {JsonConvert.SerializeObject(Spec)} Status: {JsonConvert.SerializeObject(Status ?? new object())}";
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/CustomResourceList.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Diagnostics.CodeAnalysis;
3 | using k8s.Models;
4 | using Newtonsoft.Json;
5 |
6 | namespace k8s.Operators
7 | {
8 | ///
9 | /// Represents a Kubernetes list of custom resources of type T
10 | ///
11 | [ExcludeFromCodeCoverage]
12 | public abstract class CustomResourceList : KubernetesObject where T : CustomResource
13 | {
14 | [JsonProperty("metadata")]
15 | public V1ListMeta Metadata { get; set; }
16 |
17 | [JsonProperty("items")]
18 | public List Items { get; set; }
19 | }
20 | }
--------------------------------------------------------------------------------
/tests/k8s.Operators.Tests/TestableDynamicController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using System.Threading;
3 |
4 | namespace k8s.Operators.Tests
5 | {
6 | public class TestableDynamicController : Controller
7 | {
8 | public TestableDynamicController() : base(OperatorConfiguration.Default, null, null)
9 | {
10 | }
11 |
12 | protected override Task AddOrModifyAsync(TestableDynamicCustomResource resource, CancellationToken cancellationToken)
13 | {
14 | resource.Status.property = resource.Spec.property;
15 | return Task.CompletedTask;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/samples/basic/k8s.Operators.Samples.Basic.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/samples/dynamic/k8s.Operators.Samples.Dynamic.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/k8s.Operators/Logging/SilentLogger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace k8s.Operators.Logging
6 | {
7 | ///
8 | /// Empty ILogger that doesn't log, used as fallback when no logger is passed to the library.
9 | ///
10 | [ExcludeFromCodeCoverage]
11 | internal class SilentLogger : Disposable, ILogger
12 | {
13 | public static ILogger Instance = new SilentLogger();
14 |
15 | public IDisposable BeginScope(TState state) => this;
16 |
17 | public bool IsEnabled(LogLevel logLevel) => false;
18 |
19 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
20 | {
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/tests/k8s.Operators.Tests/TestableDynamicCustomResource.cs:
--------------------------------------------------------------------------------
1 | using System.Dynamic;
2 | using k8s.Models;
3 | using k8s.Operators;
4 |
5 | namespace k8s.Operators.Tests
6 | {
7 | [CustomResourceDefinition("group", "v1", "resources")]
8 | public class TestableDynamicCustomResource : Operators.DynamicCustomResource
9 | {
10 | public TestableDynamicCustomResource() : base()
11 | {
12 | Metadata = new Models.V1ObjectMeta();
13 | Metadata.EnsureFinalizers().Add(CustomResourceDefinitionAttribute.DEFAULT_FINALIZER);
14 | Metadata.NamespaceProperty = "ns1";
15 | Metadata.Name = "resource1";
16 | Metadata.Generation = 1;
17 | Metadata.Uid = "id1";
18 |
19 | Spec = new ExpandoObject();
20 | Status = new ExpandoObject();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/samples/basic/MyResource.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace k8s.Operators.Samples.Basic
4 | {
5 | [CustomResourceDefinition("csharp-operator.example.com", "v1", "myresources")]
6 | public class MyResource : CustomResource
7 | {
8 | public class MyResourceSpec
9 | {
10 | [JsonProperty("desiredProperty")]
11 | public int Desired { get; set; }
12 | }
13 |
14 | public class MyResourceStatus
15 | {
16 | [JsonProperty("actualProperty")]
17 | public int Actual { get; set; }
18 | }
19 |
20 | public override string ToString()
21 | {
22 | return $"{Metadata.NamespaceProperty}/{Metadata.Name} (gen: {Metadata.Generation}), Spec: {Spec.Desired} Status: {Status?.Actual}";
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/tests/k8s.Operators.Tests/TestableCustomResource.cs:
--------------------------------------------------------------------------------
1 | using k8s.Operators;
2 |
3 | namespace k8s.Operators.Tests
4 | {
5 | [CustomResourceDefinition("group", "v1", "resources")]
6 | public class TestableCustomResource : Operators.CustomResource
7 | {
8 | public TestableCustomResource()
9 | {
10 | Metadata = new Models.V1ObjectMeta();
11 | Metadata.NamespaceProperty = "ns1";
12 | Metadata.Name = "resource1";
13 | Metadata.Generation = 1;
14 | Metadata.Uid = "id1";
15 | }
16 |
17 | public class TestableSpec
18 | {
19 | public string Property { get; set; }
20 | }
21 |
22 | public class TestableStatus
23 | {
24 | public string Property { get; set; }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/basic/deploy/crds/crd.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apiextensions.k8s.io/v1
2 | kind: CustomResourceDefinition
3 | metadata:
4 | name: myresources.csharp-operator.example.com
5 | spec:
6 | group: csharp-operator.example.com
7 | versions:
8 | - name: v1
9 | served: true
10 | storage: true
11 | schema:
12 | openAPIV3Schema:
13 | type: object
14 | properties:
15 | spec:
16 | type: object
17 | properties:
18 | desiredProperty:
19 | type: integer
20 | status:
21 | type: object
22 | properties:
23 | actualProperty:
24 | type: integer
25 | subresources:
26 | status: {}
27 | scope: Namespaced
28 | names:
29 | plural: myresources
30 | singular: myresource
31 | kind: MyResource
32 | shortNames:
33 | - mr
--------------------------------------------------------------------------------
/samples/dynamic/deploy/crds/crd.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apiextensions.k8s.io/v1
2 | kind: CustomResourceDefinition
3 | metadata:
4 | name: myresources.csharp-operator.example.com
5 | spec:
6 | group: csharp-operator.example.com
7 | versions:
8 | - name: v1
9 | served: true
10 | storage: true
11 | schema:
12 | openAPIV3Schema:
13 | type: object
14 | properties:
15 | spec:
16 | type: object
17 | properties:
18 | desiredProperty:
19 | type: integer
20 | status:
21 | type: object
22 | properties:
23 | actualProperty:
24 | type: integer
25 | subresources:
26 | status: {}
27 | scope: Namespaced
28 | names:
29 | plural: myresources
30 | singular: myresource
31 | kind: MyResource
32 | shortNames:
33 | - mr
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/RetryPolicy.cs:
--------------------------------------------------------------------------------
1 | namespace k8s.Operators
2 | {
3 | ///
4 | /// Represents a retry policy for a custom resource controller
5 | ///
6 | public class RetryPolicy
7 | {
8 | ///
9 | /// Max number of attempts
10 | ///
11 | public int MaxAttempts { get; set; } = 3;
12 |
13 | ///
14 | /// Initial time delay (in milliseconds) before to process again the event.
15 | /// After an attempt, the delay is incresead by multiplying it by DelayMultiplier
16 | ///
17 | public int InitialDelay { get; set; } = 5000;
18 |
19 | ///
20 | /// The multiplier applied to the delay after each attempt.
21 | /// DelayMultiplier = 1 keeps the delay constant
22 | ///
23 | public double DelayMultiplier { get; set; } = 1.5;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/CustomResourceDefinitionAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace k8s.Operators
4 | {
5 | ///
6 | /// Describe the essential custom resource definition attributes used by the Controller
7 | ///
8 | [AttributeUsage(AttributeTargets.Class)]
9 | public class CustomResourceDefinitionAttribute : Attribute
10 | {
11 | public const string DEFAULT_FINALIZER = "operator.default.finalizer";
12 |
13 | public CustomResourceDefinitionAttribute(string group, string version, string plural)
14 | {
15 | Group = group;
16 | Version = version;
17 | Plural = plural;
18 | }
19 |
20 | public string Group { get; private set; }
21 | public string Version { get; private set; }
22 | public string Plural { get; private set; }
23 | public string Finalizer { get; set; } = DEFAULT_FINALIZER;
24 | }
25 | }
--------------------------------------------------------------------------------
/samples/basic/deploy/operator.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: basic-operator
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | name: basic-operator
10 | template:
11 | metadata:
12 | labels:
13 | name: basic-operator
14 | spec:
15 | serviceAccountName: basic-operator
16 | containers:
17 | - name: basic-operator
18 | image: csharp-basic-operator
19 | imagePullPolicy: IfNotPresent
20 | env:
21 | - name: WATCH_NAMESPACE
22 | valueFrom:
23 | fieldRef:
24 | fieldPath: metadata.namespace
25 | - name: LOG_LEVEL
26 | value: "information"
27 | - name: RETRY_MAX_ATTEMPTS
28 | value: "3"
29 | - name: RETRY_INITIAL_DELAY
30 | value: "5000"
31 | - name: RETRY_DELAY_MULTIPLIER
32 | value: "1.5"
--------------------------------------------------------------------------------
/samples/dynamic/deploy/operator.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: dynamic-operator
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | name: dynamic-operator
10 | template:
11 | metadata:
12 | labels:
13 | name: dynamic-operator
14 | spec:
15 | serviceAccountName: dynamic-operator
16 | containers:
17 | - name: dynamic-operator
18 | image: csharp-dynamic-operator
19 | imagePullPolicy: IfNotPresent
20 | env:
21 | - name: WATCH_NAMESPACE
22 | valueFrom:
23 | fieldRef:
24 | fieldPath: metadata.namespace
25 | - name: LOG_LEVEL
26 | value: "information"
27 | - name: RETRY_MAX_ATTEMPTS
28 | value: "3"
29 | - name: RETRY_INITIAL_DELAY
30 | value: "5000"
31 | - name: RETRY_DELAY_MULTIPLIER
32 | value: "1.5"
--------------------------------------------------------------------------------
/src/k8s.Operators/IController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace k8s.Operators
5 | {
6 | ///
7 | /// Controller of a custom resource
8 | ///
9 | public interface IController
10 | {
11 | ///
12 | /// Processes a custom resource event
13 | ///
14 | /// The event to handle
15 | /// Signals if the current execution has been canceled
16 | Task ProcessEventAsync(CustomResourceEvent resourceEvent, CancellationToken cancellationToken);
17 |
18 | ///
19 | /// Retry policy for the controller
20 | ///
21 | RetryPolicy RetryPolicy { get; }
22 | }
23 |
24 | ///
25 | /// Controller of a custom resource of type T
26 | ///
27 | public interface IController : IController where T : CustomResource
28 | {
29 | }
30 | }
--------------------------------------------------------------------------------
/tests/k8s.Operators.Tests/k8s.Operators.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 | false
7 |
8 |
9 |
10 |
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 | all
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/samples/dynamic/README.md:
--------------------------------------------------------------------------------
1 | # C# Dynamic Kubernetes Operator
2 |
3 | The *Dynamic Operator* is a variant of the [*Basic Operator*](../basic/README.md).
4 |
5 | The `MyResource` class of the Basic Operator has been replaced with the `MyDynamicResource` class:
6 |
7 | ```csharp
8 | [CustomResourceDefinition("csharp-operator.example.com", "v1", "myresources")]
9 | public class MyDynamicResource : DynamicCustomResource
10 | {
11 | }
12 | ```
13 |
14 | A `DynamicCustomResource` doesn't force you to strongly define the schema of `Spec` and `Status` in advance ([pros and cons](https://docs.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-dynamic-net-understanding-the-dynamic-keyword-in-csharp-4)), and you can read and write any property without errors at compile time:
15 |
16 | ```csharp
17 | string x = resource.Spec.foo;
18 | resource.Status.bar = 123;
19 | ```
20 |
21 | You can run and deploy the Dynamic Operator by following the same [instructions of the Basic Operator](../basic/README.md). Just replace `basic` with `dynamic` in the paths and commands.
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/CustomResourceEvent.cs:
--------------------------------------------------------------------------------
1 | namespace k8s.Operators
2 | {
3 | ///
4 | /// Represents a custom resource event
5 | ///
6 | public class CustomResourceEvent
7 | {
8 | public CustomResourceEvent(WatchEventType type, CustomResource resource)
9 | {
10 | Type = type;
11 | Resource = resource;
12 | }
13 |
14 | ///
15 | /// The type of the event
16 | ///
17 | ///
18 | public WatchEventType Type { get; }
19 |
20 | ///
21 | /// The watched custom resource
22 | ///
23 | ///
24 | public CustomResource Resource { get; }
25 |
26 | ///
27 | /// Returns the Uid of the custom resource
28 | ///
29 | public string ResourceUid => Resource.Metadata.Uid;
30 |
31 | public override string ToString()
32 | {
33 | return $"{Type} {Resource}";
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to find out which attributes exist for C# debugging
3 | // Use hover for the description of the existing attributes
4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (console)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/samples/basic/bin/Debug/netcoreapp3.1/k8s.Operators.Samples.Basic.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}/samples/basic",
16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
17 | "console": "internalConsole",
18 | "stopAtEntry": false
19 | },
20 | {
21 | "name": ".NET Core Attach",
22 | "type": "coreclr",
23 | "request": "attach",
24 | "processId": "${command:pickProcess}"
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/Disposable.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace k8s.Operators
5 | {
6 | ///
7 | /// Represents a disposable object
8 | ///
9 | public abstract class Disposable : IDisposable
10 | {
11 | private volatile int _barrier;
12 | private volatile bool _disposing;
13 | private volatile bool _disposed;
14 |
15 | public bool IsDisposed => _disposed;
16 | public bool IsDisposing => _disposing;
17 |
18 | public void Dispose()
19 | {
20 | Dispose(true);
21 | GC.SuppressFinalize(this);
22 | }
23 |
24 | protected void Dispose(bool disposing)
25 | {
26 | if (Interlocked.CompareExchange(ref _barrier, 1, 0) == 0)
27 | {
28 | // This block can be executed only once
29 |
30 | _disposing = true;
31 |
32 | if (disposing)
33 | {
34 | DisposeInternal();
35 | }
36 |
37 | _disposing = false;
38 | _disposed = true;
39 | }
40 | }
41 |
42 | protected virtual void DisposeInternal()
43 | {
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/OperatorConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace k8s.Operators
2 | {
3 | ///
4 | /// Represents the operator configuration.
5 | ///
6 | public class OperatorConfiguration
7 | {
8 | ///
9 | /// Returns the default configuration.
10 | ///
11 | public static OperatorConfiguration Default = new OperatorConfiguration(); // TODO: make readonly
12 |
13 | ///
14 | /// The namespace to watch. Set to empty string to watch all namespaces.
15 | ///
16 | public string WatchNamespace { get; set; } = "";
17 |
18 | ///
19 | /// The label selector to filter events. Set to null to not filter.
20 | ///
21 | public string WatchLabelSelector { get; set; } = null;
22 |
23 | ///
24 | /// The retry policy for the event handling.
25 | ///
26 | public RetryPolicy RetryPolicy { get; set; } = new RetryPolicy();
27 |
28 | ///
29 | /// If true, discards the event whose spec generation has already been received and processed
30 | ///
31 | public bool DiscardDuplicateSpecGenerations { get; set; } = true;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/Models/CustomResource.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using k8s.Models;
3 | using Newtonsoft.Json;
4 |
5 | namespace k8s.Operators
6 | {
7 | ///
8 | /// Represents a Kubernetes custom resource
9 | ///
10 | public abstract class CustomResource : KubernetesObject, IKubernetesObject
11 | {
12 | [JsonProperty("metadata")]
13 | public V1ObjectMeta Metadata { get; set; }
14 |
15 | public override string ToString()
16 | {
17 | return $"{Metadata.NamespaceProperty}/{Metadata.Name} (gen: {Metadata.Generation}, uid: {Metadata.Uid})";
18 | }
19 | }
20 |
21 | ///
22 | /// Represents a Kubernetes custom resource that has a spec
23 | ///
24 | public abstract class CustomResource : CustomResource, ISpec
25 | {
26 | [JsonProperty("spec")]
27 | public TSpec Spec { get; set; }
28 | }
29 |
30 | ///
31 | /// Represents a Kubernetes custom resource that has a spec and status
32 | ///
33 | public abstract class CustomResource : CustomResource, IStatus, IStatus
34 | {
35 | [JsonProperty("status")]
36 | public TStatus Status { get; set; }
37 |
38 | object IStatus.Status { get => Status; set => Status = (TStatus) value; }
39 | }
40 | }
--------------------------------------------------------------------------------
/samples/basic/deploy/role.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: Role
3 | metadata:
4 | creationTimestamp: null
5 | name: basic-operator
6 | rules:
7 | - apiGroups:
8 | - ""
9 | resources:
10 | - pods
11 | - services
12 | - services/finalizers
13 | - endpoints
14 | - persistentvolumeclaims
15 | - events
16 | - configmaps
17 | - secrets
18 | verbs:
19 | - create
20 | - delete
21 | - get
22 | - list
23 | - patch
24 | - update
25 | - watch
26 | - apiGroups:
27 | - apps
28 | resources:
29 | - deployments
30 | - daemonsets
31 | - replicasets
32 | - statefulsets
33 | verbs:
34 | - create
35 | - delete
36 | - get
37 | - list
38 | - patch
39 | - update
40 | - watch
41 | - apiGroups:
42 | - monitoring.coreos.com
43 | resources:
44 | - servicemonitors
45 | verbs:
46 | - get
47 | - create
48 | - apiGroups:
49 | - apps
50 | resourceNames:
51 | - basic-operator
52 | resources:
53 | - deployments/finalizers
54 | verbs:
55 | - update
56 | - apiGroups:
57 | - ""
58 | resources:
59 | - pods
60 | verbs:
61 | - get
62 | - apiGroups:
63 | - apps
64 | resources:
65 | - replicasets
66 | - deployments
67 | verbs:
68 | - get
69 | - apiGroups:
70 | - csharp-operator.example.com
71 | resources:
72 | - '*'
73 | verbs:
74 | - create
75 | - delete
76 | - get
77 | - list
78 | - patch
79 | - update
80 | - watch
--------------------------------------------------------------------------------
/samples/dynamic/deploy/role.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: Role
3 | metadata:
4 | creationTimestamp: null
5 | name: dynamic-operator
6 | rules:
7 | - apiGroups:
8 | - ""
9 | resources:
10 | - pods
11 | - services
12 | - services/finalizers
13 | - endpoints
14 | - persistentvolumeclaims
15 | - events
16 | - configmaps
17 | - secrets
18 | verbs:
19 | - create
20 | - delete
21 | - get
22 | - list
23 | - patch
24 | - update
25 | - watch
26 | - apiGroups:
27 | - apps
28 | resources:
29 | - deployments
30 | - daemonsets
31 | - replicasets
32 | - statefulsets
33 | verbs:
34 | - create
35 | - delete
36 | - get
37 | - list
38 | - patch
39 | - update
40 | - watch
41 | - apiGroups:
42 | - monitoring.coreos.com
43 | resources:
44 | - servicemonitors
45 | verbs:
46 | - get
47 | - create
48 | - apiGroups:
49 | - apps
50 | resourceNames:
51 | - dynamic-operator
52 | resources:
53 | - deployments/finalizers
54 | verbs:
55 | - update
56 | - apiGroups:
57 | - ""
58 | resources:
59 | - pods
60 | verbs:
61 | - get
62 | - apiGroups:
63 | - apps
64 | resources:
65 | - replicasets
66 | - deployments
67 | verbs:
68 | - get
69 | - apiGroups:
70 | - csharp-operator.example.com
71 | resources:
72 | - '*'
73 | verbs:
74 | - create
75 | - delete
76 | - get
77 | - list
78 | - patch
79 | - update
80 | - watch
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/samples/basic/k8s.Operators.Samples.Basic.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}/samples/basic/k8s.Operators.Samples.Basic.csproj",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "${workspaceFolder}/samples/basic/k8s.Operators.Samples.Basic.csproj",
36 | "/property:GenerateFullPaths=true",
37 | "/consoleloggerparameters:NoSummary"
38 | ],
39 | "problemMatcher": "$msCompile"
40 | }
41 | ]
42 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/k8s.Operators.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | k8s.Operators
6 | 1.1.0-beta1
7 | true
8 | k8s.Operators.snk
9 |
10 |
11 |
12 | True
13 | k8s.Operators
14 | C# Operator SDK
15 | Build Kubernetes operators with C# and .NET Core
16 | Alberto Falossi
17 | kubernetes;operator;operators;c#;.net
18 | https://github.com/falox/csharp-operator-sdk
19 | https://github.com/falox/csharp-operator-sdk
20 | Apache-2.0
21 | true
22 | snupkg
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/tests/k8s.Operators.Tests/TestableOperator.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using System;
3 | using System.Linq;
4 |
5 | namespace k8s.Operators.Tests
6 | {
7 | public class TestableOperator : Operator
8 | {
9 | public TestableOperator(OperatorConfiguration configuration, IKubernetes client, ILoggerFactory loggerFactory = null) : base(configuration, client, loggerFactory)
10 | {
11 | }
12 |
13 | ///
14 | /// Simulates an incoming event for a given controller
15 | ///
16 | public void SimulateEvent(IController controller, WatchEventType eventType, CustomResource resource)
17 | {
18 | _watchers.Single(x => x.Controller == controller).OnIncomingEvent(eventType, resource);
19 | }
20 |
21 | ///
22 | /// Protected method exposed as Public
23 | ///
24 | public void Exposed_OnWatchError(Exception exception) => OnWatcherError(exception);
25 |
26 | protected override void OnWatcherClose()
27 | {
28 | // HACK: Any watcher will fail and close during tests, since the external Watcher class is not mocked at the moment.
29 | // This override will ignore the close event and prevent the operator to be stopped prematurely
30 | }
31 |
32 | public int DisposeInvocationCount { get; private set; }
33 |
34 | protected override void DisposeInternal()
35 | {
36 | base.DisposeInternal();
37 |
38 | DisposeInvocationCount++;
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/Logging/ConsoleTracingInterceptor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Diagnostics.CodeAnalysis;
5 | using System.Net.Http;
6 | using Microsoft.Rest;
7 |
8 | namespace k8s.Operators.Logging
9 | {
10 | ///
11 | /// A console tracer for the ServiceClientTracing service used by the Kubernetes C# client
12 | ///
13 | ///
14 | [ExcludeFromCodeCoverage]
15 | public class ConsoleTracingInterceptor : IServiceClientTracingInterceptor
16 | {
17 | public void Information(string message)
18 | {
19 | Console.WriteLine(message);
20 | }
21 |
22 | public void TraceError(string invocationId, Exception exception)
23 | {
24 | Console.WriteLine($"invocationId: {invocationId}, exception: {exception}");
25 | }
26 |
27 | public void ReceiveResponse(string invocationId, HttpResponseMessage response)
28 | {
29 | Console.WriteLine($"invocationId: {invocationId}\r\nresponse: {(response == null ? string.Empty : response.AsFormattedString())}");
30 | }
31 |
32 | public void SendRequest(string invocationId, HttpRequestMessage request)
33 | {
34 | Console.WriteLine($"invocationId: {invocationId}\r\nrequest: {(request == null ? string.Empty : request.AsFormattedString())}");
35 | }
36 |
37 | public void Configuration(string source, string name, string value)
38 | {
39 | Console.WriteLine($"Configuration: source={source}, name={name}, value={value}");
40 | }
41 |
42 | public void EnterMethod(string invocationId, object instance, string method, IDictionary parameters)
43 | {
44 | Console.WriteLine($"invocationId: {invocationId}\r\ninstance: {instance}\r\nmethod: {method}\r\nparameters: {parameters.AsFormattedString()}");
45 | }
46 |
47 | public void ExitMethod(string invocationId, object returnValue)
48 | {
49 | Console.WriteLine($"invocationId: {invocationId}, return value: {(returnValue == null ? string.Empty : returnValue.ToString())}");
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/IOperator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 |
4 | namespace k8s.Operators
5 | {
6 | ///
7 | /// Represents a Kubernetes operator
8 | ///
9 | public interface IOperator : IDisposable
10 | {
11 | ///
12 | /// Adds a controller to handle the events of the custom resource R
13 | ///
14 | /// The controller for the custom resource
15 | /// The watched namespace. Set to null to watch all namespaces
16 | /// The label selector to filter the sets of events returned/>
17 | /// The type of the custom resource
18 | IOperator AddController(IController controller, string watchNamespace = "default", string labelSelector = null) where R : CustomResource;
19 |
20 | ///
21 | /// Adds a new instance of a controller of type C to handle the events of the custom resource
22 | ///
23 | /// The type of the controller. C must implement IController and expose a constructor that accepts (OperatorConfiguration, IKubernetes, ILoggerFactory)
24 | /// The instance of the controller
25 | IController AddControllerOfType() where C : IController;
26 |
27 | ///
28 | /// Starts watching and handling events
29 | ///
30 | Task StartAsync();
31 |
32 | ///
33 | /// Stops the operator and release the resources. Once stopped, an operator cannot be restarted. Stop() is an alias for Dispose()
34 | ///
35 | void Stop();
36 |
37 | ///
38 | /// Returns true if StartAsync has been called and the operator is running
39 | ///
40 | bool IsRunning { get; }
41 |
42 | ///
43 | /// Returns true if Stop/Dispose has been called and not completed
44 | ///
45 | ///
46 | bool IsDisposing { get; }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/EventWatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace k8s.Operators
6 | {
7 | ///
8 | /// Implements the watch callback method for a given
9 | ///
10 | public class EventWatcher
11 | {
12 | private readonly ILogger _logger;
13 | private readonly CancellationToken _cancellationToken;
14 |
15 | public Type ResourceType { get; private set; }
16 | public CustomResourceDefinitionAttribute CRD { get; private set; }
17 | public string Namespace { get; private set; }
18 | public string LabelSelector { get; private set; }
19 | public IController Controller { get; private set; }
20 |
21 | public EventWatcher(Type resourceType, string @namespace, string labelSelector, IController controller, ILogger logger, CancellationToken cancellationToken)
22 | {
23 | this.ResourceType = resourceType;
24 | this.Namespace = @namespace;
25 | this.LabelSelector = labelSelector;
26 | this.Controller = controller;
27 | this._logger = logger;
28 | this._cancellationToken = cancellationToken;
29 |
30 | // Retrieve the CRD associated to the CR
31 | var crd = (CustomResourceDefinitionAttribute)Attribute.GetCustomAttribute(resourceType, typeof(CustomResourceDefinitionAttribute));
32 | this.CRD = crd;
33 | }
34 |
35 | ///
36 | /// Dispatches an incoming event to the controller
37 | ///
38 | public void OnIncomingEvent(WatchEventType eventType, CustomResource resource)
39 | {
40 | var resourceEvent = new CustomResourceEvent(eventType, resource);
41 |
42 | _logger.LogDebug($"Received event {resourceEvent}");
43 |
44 | Controller.ProcessEventAsync(resourceEvent, _cancellationToken)
45 | .ContinueWith(t =>
46 | {
47 | if (t.IsFaulted)
48 | {
49 | var exception = t.Exception.Flatten().InnerException;
50 | _logger.LogError(exception, $"Error processing {resourceEvent}");
51 | }
52 | });
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/src/k8s.Operators/ResourceChangeTracker.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using k8s.Operators.Logging;
3 | using Microsoft.Extensions.Logging;
4 |
5 | namespace k8s.Operators
6 | {
7 | ///
8 | /// Keeps track of the resource changes to avoid unnecessary updates
9 | ///
10 | public class ResourceChangeTracker
11 | {
12 | private readonly ILogger _logger;
13 |
14 | // Last generation number successfully processed, for each resource
15 | private readonly Dictionary _lastResourceGenerationProcessed;
16 | private readonly bool _discardDuplicates;
17 |
18 | public ResourceChangeTracker(OperatorConfiguration configuration, ILoggerFactory loggerFactory)
19 | {
20 | this._logger = loggerFactory?.CreateLogger() ?? SilentLogger.Instance;
21 | this._lastResourceGenerationProcessed = new Dictionary();
22 | this._discardDuplicates = configuration.DiscardDuplicateSpecGenerations;
23 | }
24 |
25 | ///
26 | /// Returns true if the same resource/generation has already been handled
27 | ///
28 | public bool IsResourceGenerationAlreadyHandled(CustomResource resource)
29 | {
30 | if (_discardDuplicates)
31 | {
32 | bool processedInPast = _lastResourceGenerationProcessed.TryGetValue(resource.Metadata.Uid, out long resourceGeneration);
33 |
34 | return processedInPast
35 | && resource.Metadata.Generation != null
36 | && resourceGeneration >= resource.Metadata.Generation.Value;
37 | }
38 | else
39 | {
40 | return false;
41 | }
42 | }
43 |
44 | ///
45 | /// Mark a resource generation as successfully handled
46 | ///
47 | public void TrackResourceGenerationAsHandled(CustomResource resource)
48 | {
49 | if (resource.Metadata.Generation != null)
50 | {
51 | _lastResourceGenerationProcessed[resource.Metadata.Uid] = resource.Metadata.Generation.Value;
52 | }
53 | }
54 |
55 | ///
56 | /// Mark a resource generation as successfully deleted
57 | ///
58 | public void TrackResourceGenerationAsDeleted(CustomResource resource)
59 | {
60 | _lastResourceGenerationProcessed.Remove(resource.Metadata.Uid);
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/samples/basic/MyResourceController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using k8s.Models;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace k8s.Operators.Samples.Basic
8 | {
9 | public class MyResourceController : Controller
10 | {
11 | public MyResourceController(OperatorConfiguration configuration, IKubernetes client, ILoggerFactory loggerFactory = null) : base(configuration, client, loggerFactory)
12 | {
13 | }
14 |
15 | protected override async Task AddOrModifyAsync(MyResource resource, CancellationToken cancellationToken)
16 | {
17 | _logger.LogInformation($"Begin AddOrModify {resource}");
18 |
19 | try
20 | {
21 | // Simulate event handling
22 | await Task.Delay(5000, cancellationToken);
23 |
24 | // Update the resource
25 | resource.Metadata.EnsureAnnotations()["custom-key"] = DateTime.UtcNow.ToString("s");
26 | await UpdateResourceAsync(resource, cancellationToken);
27 |
28 | // Update the status
29 | if (resource.Status == null)
30 | {
31 | resource.Status = new MyResource.MyResourceStatus();
32 | }
33 | if (resource.Status.Actual != resource.Spec.Desired)
34 | {
35 | resource.Status.Actual = resource.Spec.Desired;
36 | await UpdateStatusAsync(resource, cancellationToken);
37 | }
38 | }
39 | catch (OperationCanceledException)
40 | {
41 | _logger.LogInformation($"Interrupted! Trying to shutdown gracefully...");
42 |
43 | // Simulate a blocking operation
44 | Task.Delay(3000).Wait();
45 | }
46 |
47 | _logger.LogInformation($"End AddOrModify {resource}");
48 | }
49 |
50 | protected override async Task DeleteAsync(MyResource resource, CancellationToken cancellationToken)
51 | {
52 | _logger.LogInformation($"Begin Delete {resource}");
53 |
54 | try
55 | {
56 | // Simulate event handling
57 | await Task.Delay(5000, cancellationToken);
58 | }
59 | catch (OperationCanceledException)
60 | {
61 | _logger.LogInformation($"Interrupted! Trying to shutdown gracefully...");
62 |
63 | // Simulate a blocking operation
64 | Task.Delay(3000).Wait();
65 | }
66 |
67 | _logger.LogInformation($"End Delete {resource}");
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/.github/workflows/dotnet-core.yml:
--------------------------------------------------------------------------------
1 | name: .NET Core
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | release:
9 | types: [ published ]
10 |
11 | env:
12 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
13 | DOTNET_CLI_TELEMETRY_OPTOUT: true
14 | # GITHUB_FEED: https://nuget.pkg.github.com/falox/
15 | # GITHUB_USER: falox
16 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 | NUGET_FEED: https://api.nuget.org/v3/index.json
18 | NUGET_KEY: ${{ secrets.NUGET_KEY }}
19 |
20 | jobs:
21 | build:
22 |
23 | runs-on: ubuntu-latest
24 |
25 | steps:
26 | - uses: actions/checkout@v2
27 | - name: Setup .NET Core
28 | uses: actions/setup-dotnet@v1
29 | with:
30 | dotnet-version: 3.1.301
31 | - name: Install dependencies
32 | run: dotnet restore
33 | - name: Build
34 | run: dotnet build --configuration Release --no-restore
35 | - name: Test
36 | run: dotnet test --no-restore --verbosity normal
37 | - name: Generate coverage report
38 | run: |
39 | cd ./tests/k8s.Operators.Tests/
40 | dotnet test /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov
41 | - name: Publish coverage report to coveralls.io
42 | uses: coverallsapp/github-action@master
43 | with:
44 | github-token: ${{ secrets.GITHUB_TOKEN }}
45 | path-to-lcov: ./tests/k8s.Operators.Tests/TestResults/coverage.info
46 | - name: Pack
47 | run: dotnet pack --configuration Release --no-restore --verbosity normal
48 | - name: Upload Artifact
49 | uses: actions/upload-artifact@v2
50 | with:
51 | name: nupkg
52 | path: ./src/k8s.Operators/bin/Release/*.nupkg
53 |
54 | deploy:
55 | needs: build
56 | if: github.event_name == 'release'
57 |
58 | runs-on: ubuntu-latest
59 |
60 | steps:
61 | - uses: actions/checkout@v2
62 | - name: Setup .NET Core
63 | uses: actions/setup-dotnet@v1
64 | with:
65 | dotnet-version: 3.1.301
66 | - name: Create Release NuGet package
67 | run: |
68 | arrTag=(${GITHUB_REF//\// })
69 | VERSION="${arrTag[2]}"
70 | echo Version: $VERSION
71 | VERSION="${VERSION//v}"
72 | echo Clean Version: $VERSION
73 | dotnet pack -v normal -c Release -p:PackageVersion=$VERSION -o nupkg src/k8s.Operators/k8s.Operators.*proj
74 | #- name: Push to GitHub Feed
75 | # run: |
76 | # for f in ./nupkg/*.nupkg
77 | # do
78 | # curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED
79 | # done
80 | - name: Push to NuGet Feed
81 | run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --skip-duplicate --api-key $NUGET_KEY
82 |
--------------------------------------------------------------------------------
/samples/dynamic/MyDynamicResourceController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using System.Dynamic;
5 | using k8s.Models;
6 | using Microsoft.Extensions.Logging;
7 |
8 | namespace k8s.Operators.Samples.Dynamic
9 | {
10 | public class MyDynamicResourceController : Controller
11 | {
12 | public MyDynamicResourceController(OperatorConfiguration configuration, IKubernetes client, ILoggerFactory loggerFactory = null) : base(configuration, client, loggerFactory)
13 | {
14 | }
15 |
16 | protected override async Task AddOrModifyAsync(MyDynamicResource resource, CancellationToken cancellationToken)
17 | {
18 | _logger.LogInformation($"Begin AddOrModify {resource}");
19 |
20 | try
21 | {
22 | // Simulate event handling
23 | await Task.Delay(5000, cancellationToken);
24 |
25 | // Update the resource
26 | resource.Metadata.EnsureAnnotations()["custom-key"] = DateTime.UtcNow.ToString("s");
27 | await UpdateResourceAsync(resource, cancellationToken);
28 |
29 | // Update the status
30 | if (resource.Status?.actualProperty != resource.Spec.desiredProperty)
31 | {
32 | if (resource.Status == null)
33 | {
34 | resource.Status = new ExpandoObject();
35 | }
36 | resource.Status.actualProperty = resource.Spec.desiredProperty;
37 | await UpdateStatusAsync(resource, cancellationToken);
38 | }
39 | }
40 | catch (OperationCanceledException)
41 | {
42 | _logger.LogInformation($"Interrupted! Trying to shutdown gracefully...");
43 |
44 | // Simulate a blocking operation
45 | Task.Delay(3000).Wait();
46 | }
47 |
48 | _logger.LogInformation($"End AddOrModify {resource}");
49 | }
50 |
51 | protected override async Task DeleteAsync(MyDynamicResource resource, CancellationToken cancellationToken)
52 | {
53 | _logger.LogInformation($"Begin Delete {resource}");
54 |
55 | try
56 | {
57 | // Simulate event handling
58 | await Task.Delay(5000, cancellationToken);
59 | }
60 | catch (OperationCanceledException)
61 | {
62 | _logger.LogInformation($"Interrupted! Trying to shutdown gracefully...");
63 |
64 | // Simulate a blocking operation
65 | Task.Delay(3000).Wait();
66 | }
67 |
68 | _logger.LogInformation($"End Delete {resource}");
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/samples/basic/README.md:
--------------------------------------------------------------------------------
1 | # C# Basic Kubernetes Operator
2 |
3 | The *Basic Operator* handles the `MyResource` custom resource. The operator simulates the interaction with an external service and can be used as a template for real-world operators.
4 |
5 | Once the operator detects an added/modified event, it waits for 5 seconds and:
6 |
7 | - Adds a custom annotation `custom-key` in the object's `metadata.annotation`
8 | - Updates the `status.actualProperty` to match the `spec.desiredProperty`
9 |
10 | See the implementation of `MyResourceController.cs` for more details.
11 |
12 | ## Prerequisites
13 |
14 | - [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)
15 | - [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
16 | - [Kubernetes](https://kubernetes.io/docs/setup/)
17 |
18 | ## Running locally
19 |
20 | 1. Add the custom resource definition to your Kubernetes cluster:
21 |
22 | ```bash
23 | $ cd csharp-operator-sdk/samples/basic
24 | $ kubectl apply -f ./deploy/crds/crd.yaml
25 | ```
26 |
27 | 2. Compile and run the operator:
28 | ```bash
29 | $ dotnet build
30 | $ dotnet run
31 | ```
32 |
33 | The operator will connect to the Kubernetes cluster and will start watching for events. You'll see something similar to:
34 |
35 | ```
36 | <6>k8s.Operators.Operator[0] Start operator
37 | ```
38 |
39 | 3. In another terminal, create a new `MyResource` object:
40 |
41 | ```bash
42 | $ kubectl apply -f ./deploy/crds/cr.yaml
43 | ```
44 |
45 | The operator will detect the new object and after 5 seconds will update the *status* to match the desired *spec*:
46 |
47 | ```
48 | <6>k8s.Operators.Controller[0] Begin AddOrModify default/mr1 (gen: 1), Spec: 1 Status:
49 | <6>k8s.Operators.Controller[0] End AddOrModify default/mr1 (gen: 1), Spec: 1 Status: 1
50 | ```
51 |
52 | 4. Edit the resource and change the `spec.desiredProperty` to `2`:
53 |
54 | ```bash
55 | $ kubectl edit myresources mr1
56 | ```
57 |
58 | ```yaml
59 | apiVersion: csharp-operator.example.com/v1
60 | kind: MyResource
61 | :
62 | spec:
63 | desiredProperty: 2
64 | :
65 | ```
66 |
67 | The operator will detect the change and will align again the *status*:
68 |
69 | ```
70 | <6>k8s.Operators.Controller[0] Begin AddOrModify default/mr1 (gen: 2), Spec: 2 Status: 1
71 | <6>k8s.Operators.Controller[0] End AddOrModify default/mr1 (gen: 2), Spec: 2 Status: 2
72 | ```
73 |
74 | 5. Delete the resource:
75 |
76 | ```bash
77 | $ kubectl delete myresources mr1
78 | ```
79 |
80 | The operator will simulate the deletion of the resource:
81 |
82 | ```
83 | <6>k8s.Operators.Controller[0] Begin Delete default/mr1 (gen: 3), Spec: 2 Status: 2
84 | <6>k8s.Operators.Controller[0] End Delete default/mr1 (gen: 3), Spec: 2 Status: 2
85 | ```
86 |
87 | 6. Shutdown the operator with CTRL+C or by sending a SIGTERM signal with:
88 |
89 | ```bash
90 | kill $(ps aux | grep '[k]8s.Operators.Samples.Basic' | awk '{print $2}')
91 | ```
92 |
93 | The operator will gracefully shutdown:
94 |
95 | ```
96 | <6>k8s.Operators.Operator[0] Stop operator
97 | <6>k8s.Operators.Operator[0] Disposing operator
98 | ```
99 |
100 | ## Deploy the operator in Kubernetes
101 |
102 | 1. If you are running [Minikube](https://kubernetes.io/docs/setup/learning-environment/minikube/), point your shell to minikube's docker-daemon:
103 |
104 | ```bash
105 | $ eval $(minikube -p minikube docker-env)
106 | ```
107 |
108 | 2. Create the docker image:
109 |
110 | ```bash
111 | $ cd csharp-operator-sdk
112 | $ docker build -t csharp-basic-operator -f samples/basic/Dockerfile .
113 | ```
114 |
115 | 3. Add the custom resource definition to your Kubernetes cluster:
116 |
117 | ```bash
118 | cd samples/basic
119 | kubectl apply -f ./deploy/crds/crd.yaml
120 | ```
121 |
122 | 4. Deploy the operator:
123 |
124 | ```bash
125 | kubectl apply -f ./deploy/service_account.yaml
126 | kubectl apply -f ./deploy/role.yaml
127 | kubectl apply -f ./deploy/role_binding.yaml
128 | kubectl apply -f ./deploy/operator.yaml
129 | ```
--------------------------------------------------------------------------------
/src/k8s.Operators/EventManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using k8s.Operators.Logging;
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace k8s.Operators
7 | {
8 | ///
9 | /// Manages the event queues for the watched resources
10 | ///
11 | public class EventManager
12 | {
13 | private ILogger _logger;
14 |
15 | // Next event to handle, for each resource.
16 | // A real queue is not used since intermediate events are discarded and only the queue head is stored.
17 | private Dictionary _queuesByResource;
18 |
19 | // Events that are currently being handled
20 | private Dictionary _handling;
21 |
22 | public EventManager(ILoggerFactory loggerFactory)
23 | {
24 | this._logger = loggerFactory?.CreateLogger() ?? SilentLogger.Instance;
25 | this._queuesByResource = new Dictionary();
26 | this._handling = new Dictionary();
27 | }
28 |
29 | ///
30 | /// Enqueue the event
31 | ///
32 | public void Enqueue(CustomResourceEvent resourceEvent)
33 | {
34 | lock (this)
35 | {
36 | _logger.LogTrace($"Enqueue {resourceEvent}");
37 | // Insert or update the next event for the resource
38 | _queuesByResource[resourceEvent.ResourceUid] = resourceEvent;
39 | }
40 | }
41 |
42 | ///
43 | /// Returns the next event to process, without dequeuing it
44 | ///
45 | public CustomResourceEvent Peek(string resourceUid)
46 | {
47 | lock (this)
48 | {
49 | if (_queuesByResource.TryGetValue(resourceUid, out CustomResourceEvent result))
50 | {
51 | _logger.LogTrace($"Peek {result}");
52 | }
53 | return result;
54 | }
55 | }
56 |
57 | ///
58 | /// Pops and returns the next event to process, if any
59 | ///
60 | public CustomResourceEvent Dequeue(string resourceUid)
61 | {
62 | lock (this)
63 | {
64 | if (IsHandling(resourceUid, out var handlingEvent))
65 | {
66 | _logger.LogDebug($"Postponed Dequeue, handling {handlingEvent}");
67 | return null;
68 | }
69 | else
70 | {
71 | if (_queuesByResource.TryGetValue(resourceUid, out CustomResourceEvent result))
72 | {
73 | _queuesByResource.Remove(resourceUid);
74 | _logger.LogTrace($"Dequeue {result}");
75 | }
76 | return result;
77 | }
78 | }
79 | }
80 |
81 | ///
82 | /// Track the begin of an event handling
83 | ///
84 | public void BeginHandleEvent(CustomResourceEvent resourceEvent)
85 | {
86 | lock (this)
87 | {
88 | _logger.LogTrace($"BeginHandleEvent {resourceEvent}");
89 | _handling[resourceEvent.ResourceUid] = resourceEvent;
90 | }
91 | }
92 |
93 | ///
94 | /// Track the end of an event handling
95 | ///
96 | public void EndHandleEvent(CustomResourceEvent resourceEvent)
97 | {
98 | lock (this)
99 | {
100 | _logger.LogTrace($"EndHandleEvent {resourceEvent}");
101 | _handling.Remove(resourceEvent.ResourceUid);
102 | }
103 | }
104 |
105 | ///
106 | /// Returns true if there is an event being handled
107 | ///
108 | private bool IsHandling(string resourceUid, out CustomResourceEvent handlingEvent)
109 | {
110 | return _handling.TryGetValue(resourceUid, out handlingEvent);
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/tests/k8s.Operators.Tests/TestableController.cs:
--------------------------------------------------------------------------------
1 | using k8s.Operators;
2 | using Microsoft.Extensions.Logging;
3 | using System.Threading.Tasks;
4 | using System.Collections.Generic;
5 | using System.Threading;
6 | using System;
7 |
8 | namespace k8s.Operators.Tests
9 | {
10 | public class TestableController : Controller
11 | {
12 | public TestableController() : base(OperatorConfiguration.Default, null, null)
13 | {
14 | }
15 |
16 | public TestableController(IKubernetes client, ILoggerFactory loggerFactory = null) : base(OperatorConfiguration.Default, client, loggerFactory)
17 | {
18 | }
19 |
20 | public TestableController(OperatorConfiguration configuration, IKubernetes client, ILoggerFactory loggerFactory = null) : base(configuration, client, loggerFactory)
21 | {
22 | }
23 |
24 | public List Invocations_AddOrModify = new List();
25 | public List Invocations_Delete = new List();
26 | public List<(TestableCustomResource resource, bool deleteEvent)> Invocations = new List<(TestableCustomResource resource, bool deleteEvent)>();
27 | public List<(TestableCustomResource resource, bool deleteEvent)> CompletedEvents = new List<(TestableCustomResource resource, bool deleteEvent)>();
28 |
29 | private Queue> _signals = new Queue>();
30 |
31 | protected override async Task AddOrModifyAsync(TestableCustomResource resource, CancellationToken cancellationToken)
32 | {
33 | Invocations_AddOrModify.Add(resource);
34 | Invocations.Add((resource, false));
35 |
36 | if (_signals.TryDequeue(out var signal))
37 | {
38 | // Wait for UnblockEvent()
39 | await signal?.Task;
40 | }
41 |
42 | if (_exceptionsToThrow > 0)
43 | {
44 | _exceptionsToThrow--;
45 | throw new Exception();
46 | }
47 |
48 | CompletedEvents.Add((resource, deleteEvent: false));
49 | }
50 |
51 | protected override async Task DeleteAsync(TestableCustomResource resource, CancellationToken cancellationToken)
52 | {
53 | Invocations_Delete.Add(resource);
54 | Invocations.Add((resource, true));
55 |
56 | if (_signals.TryDequeue(out var signal))
57 | {
58 | // Wait for UnblockEvent()
59 | await signal?.Task;
60 | }
61 |
62 | if (_exceptionsToThrow > 0)
63 | {
64 | _exceptionsToThrow--;
65 | throw new Exception();
66 | }
67 |
68 | CompletedEvents.Add((resource, deleteEvent: true));
69 | }
70 |
71 | ///
72 | /// Protected method exposed as Public
73 | ///
74 | public Task Exposed_UpdateResourceAsync(TestableCustomResource resource, CancellationToken cancellationToken) => UpdateResourceAsync(resource, cancellationToken);
75 |
76 | ///
77 | /// Protected method exposed as Public
78 | ///
79 | public Task Exposed_UpdateStatusAsync(TestableCustomResource resource, CancellationToken cancellationToken) => UpdateStatusAsync(resource, cancellationToken);
80 |
81 | ///
82 | /// Throws an exception in the next calls to AddOrModifyAsync or DeleteAsync
83 | ///
84 | /// The number of the events to make fail
85 | public void ThrowExceptionOnNextEvents(int count)
86 | {
87 | _exceptionsToThrow = count;
88 | }
89 |
90 | private int _exceptionsToThrow = 0;
91 |
92 | ///
93 | /// Block the next call to AddOrModifyAsync or DeleteAsync
94 | ///
95 | public TaskCompletionSource