Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/build-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ jobs:
run: dotnet build -c Release

- name: Test
run: dotnet test -c Release
run: |
for project in test/*/*.csproj; do
echo "Running tests for $project..."
dotnet run --project "$project" -c Release
done

- name: DotNet Pack
if: startsWith(github.ref, 'refs/tags/')
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: csharp

Expand All @@ -41,4 +41,4 @@ jobs:
run: dotnet build --configuration Release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
27 changes: 0 additions & 27 deletions Directory.Packages.props

This file was deleted.

12 changes: 12 additions & 0 deletions K8sOperator.NET.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Solution>
<Folder Name="/examples/">
<Project Path="examples/SimpleOperator/SimpleOperator.csproj" Id="ed579abf-9eb5-4f74-95e3-8f4ac03f760a" />
</Folder>
<Folder Name="/src/">
<Project Path="src/K8sOperator.NET/K8sOperator.NET.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test/K8sOperator.NET.Target.Tests/K8sOperator.NET.Target.Tests.csproj" />
<Project Path="test/K8sOperator.NET.Tests/K8sOperator.NET.Tests.csproj" />
</Folder>
</Solution>
79 changes: 0 additions & 79 deletions K8sOperator.sln

This file was deleted.

10 changes: 10 additions & 0 deletions examples/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<!-- Import the parent Directory.Build.props if it exists -->
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="Exists($([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')))"/>

<!-- For example projects that reference K8sOperator.NET as a project reference -->
<Import Project="$(MSBuildThisFileDirectory)..\src\K8sOperator.NET\Operator.targets"
Condition="Exists('$(MSBuildThisFileDirectory)..\src\K8sOperator.NET\Operator.targets')" />
</Project>
48 changes: 48 additions & 0 deletions examples/SimpleOperator/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Git
.git
.gitignore
.gitattributes

# Build results
bin/
obj/
[Bb]uild/
[Dd]ebug/
[Rr]elease/

# Visual Studio
.vs/
.vscode/
*.user
*.suo
*.userosscache
*.sln.docstates

# Test results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
TestResults/

# NuGet
*.nupkg
*.snupkg
packages/

# Docker
Dockerfile
.dockerignore

# Kubernetes
*.yaml
*.yml

# Documentation
*.md
README*
LICENSE

# IDE
.idea/
*.swp
*.swo
*~
89 changes: 89 additions & 0 deletions examples/SimpleOperator/Controllers/TodoController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using K8sOperator.NET;
using SimpleOperator.Resources;

namespace SimpleOperator.Controllers;

public class TodoController : OperatorController<TodoItem>
{
private readonly ILogger<TodoController> _logger;

public TodoController(ILogger<TodoController> logger)
{
_logger = logger;
}

public override async Task AddOrModifyAsync(TodoItem resource, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Processing TodoItem: {Name} - Title: {Title}, Priority: {Priority}",
resource.Metadata.Name,
resource.Spec.Title,
resource.Spec.Priority);

// Initialize status if needed
if (resource.Status == null)
{
resource.Status = new TodoItem.TodoStatus();
}

// Update reconciliation count
resource.Status.ReconciliationCount++;

// Business logic: Auto-complete if due date is passed
if (resource.Spec.DueDate.HasValue &&
resource.Spec.DueDate.Value < DateTime.UtcNow &&
resource.Status.State != "completed")
Comment on lines +15 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against null Spec to avoid reconciliation crashes.

Line 18 and Line 33 dereference resource.Spec without a null check. A malformed CRD will throw and break the controller loop.

🛠️ Suggested fix
     public override async Task AddOrModifyAsync(TodoItem resource, CancellationToken cancellationToken)
     {
+        if (resource.Spec is null)
+        {
+            _logger.LogWarning(
+                "TodoItem {Name} has no spec; skipping reconciliation",
+                resource.Metadata?.Name ?? "<unknown>");
+            return;
+        }
+
         _logger.LogInformation(
             "Processing TodoItem: {Name} - Title: {Title}, Priority: {Priority}",
             resource.Metadata.Name,
             resource.Spec.Title,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public override async Task AddOrModifyAsync(TodoItem resource, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Processing TodoItem: {Name} - Title: {Title}, Priority: {Priority}",
resource.Metadata.Name,
resource.Spec.Title,
resource.Spec.Priority);
// Initialize status if needed
if (resource.Status == null)
{
resource.Status = new TodoItem.TodoStatus();
}
// Update reconciliation count
resource.Status.ReconciliationCount++;
// Business logic: Auto-complete if due date is passed
if (resource.Spec.DueDate.HasValue &&
resource.Spec.DueDate.Value < DateTime.UtcNow &&
resource.Status.State != "completed")
public override async Task AddOrModifyAsync(TodoItem resource, CancellationToken cancellationToken)
{
if (resource.Spec is null)
{
_logger.LogWarning(
"TodoItem {Name} has no spec; skipping reconciliation",
resource.Metadata?.Name ?? "<unknown>");
return;
}
_logger.LogInformation(
"Processing TodoItem: {Name} - Title: {Title}, Priority: {Priority}",
resource.Metadata.Name,
resource.Spec.Title,
resource.Spec.Priority);
// Initialize status if needed
if (resource.Status == null)
{
resource.Status = new TodoItem.TodoStatus();
}
// Update reconciliation count
resource.Status.ReconciliationCount++;
// Business logic: Auto-complete if due date is passed
if (resource.Spec.DueDate.HasValue &&
resource.Spec.DueDate.Value < DateTime.UtcNow &&
resource.Status.State != "completed")
🤖 Prompt for AI Agents
In `@examples/SimpleOperator/Controllers/TodoController.cs` around lines 15 - 35,
Add a null-guard for resource.Spec at the start of AddOrModifyAsync (method in
TodoController) to avoid dereferencing a malformed CRD: check if resource.Spec
is null, handle it by initializing a safe default Spec or setting
resource.Status appropriately (e.g., mark error or increment ReconciliationCount
and set State), log a warning including resource.Metadata.Name, and then return
or continue without accessing resource.Spec; ensure subsequent uses of
resource.Spec (e.g., Spec.Title, Spec.Priority, Spec.DueDate) only occur after
this guard so AddOrModifyAsync and updates to
resource.Status.ReconciliationCount and State are safe.

{
resource.Status.State = "overdue";
resource.Status.Message = $"Task is overdue by {(DateTime.UtcNow - resource.Spec.DueDate.Value).Days} days";
_logger.LogWarning("TodoItem {Name} is overdue!", resource.Metadata.Name);
}
else if (resource.Status.State == "pending")
{
resource.Status.State = "in-progress";
resource.Status.Message = "Task is being processed";
_logger.LogInformation("TodoItem {Name} moved to in-progress", resource.Metadata.Name);
}

// Simulate some async work
await Task.Delay(100, cancellationToken);
}

public override async Task DeleteAsync(TodoItem resource, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Deleting TodoItem: {Name}. Final state was: {State}",
resource.Metadata.Name,
resource.Status?.State ?? "unknown");

// Cleanup logic here (e.g., remove external resources)
await Task.CompletedTask;
}

public override async Task FinalizeAsync(TodoItem resource, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Finalizing TodoItem: {Name}",
resource.Metadata.Name);

// Perform cleanup before resource is completely deleted
// For example: remove related resources, notify external systems, etc.

await Task.CompletedTask;
}

public override async Task ErrorAsync(TodoItem resource, CancellationToken cancellationToken)
{
_logger.LogError(
"Error occurred for TodoItem: {Name}",
resource.Metadata.Name);

if (resource.Status != null)
{
resource.Status.State = "error";
resource.Status.Message = "An error occurred during reconciliation";
}

await Task.CompletedTask;
}
}
44 changes: 44 additions & 0 deletions examples/SimpleOperator/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy project file and restore dependencies
COPY ["SimpleOperator.csproj", "./"]
RUN dotnet restore

# Copy source code and build
COPY . .
RUN dotnet build -c Release -o /app/build

# Publish stage
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app

# Create non-root user
RUN groupadd -r operator && useradd -r -g operator operator

# Copy published app
COPY --from=publish /app/publish .

# Set ownership
RUN chown -R operator:operator /app

# Switch to non-root user
USER operator

# Set environment variables
ENV ASPNETCORE_ENVIRONMENT=Production \
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
DOTNET_EnableDiagnostics=0

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD dotnet SimpleOperator.dll version || exit 1
Comment on lines +38 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find examples/SimpleOperator -type f -name "*.cs" | head -20

Repository: pmdevers/K8sOperator.NET

Length of output: 202


🏁 Script executed:

rg -n "version" examples/SimpleOperator/ --type cs -B 2 -A 2

Repository: pmdevers/K8sOperator.NET

Length of output: 50


🏁 Script executed:

# Also check if there's a Program.cs or main entry point
find examples/SimpleOperator -name "Program.cs" -o -name "Program.fs" -o -name "Main.cs"

Repository: pmdevers/K8sOperator.NET

Length of output: 102


🏁 Script executed:

cat -n examples/SimpleOperator/Program.cs

Repository: pmdevers/K8sOperator.NET

Length of output: 389


Fix the health check command—the version subcommand does not exist.

The HEALTHCHECK command attempts to run dotnet SimpleOperator.dll version, but the application (Program.cs) is a standard ASP.NET Core web server with no CLI subcommand handling. It runs app.RunOperatorAsync() and expects to accept the operator argument at startup. The health check should instead use an HTTP request to verify the application is responding, e.g., CMD curl -f http://localhost:8080/health || exit 1 (if a health endpoint is implemented), or remove the health check entirely and rely on Kubernetes liveness probes.

🤖 Prompt for AI Agents
In `@examples/SimpleOperator/Dockerfile` around lines 38 - 40, The HEALTHCHECK
currently runs "dotnet SimpleOperator.dll version" which is invalid because
Program.cs runs the ASP.NET Core server via app.RunOperatorAsync() and expects
the "operator" startup argument; update the Dockerfile HEALTHCHECK to verify the
HTTP server instead (e.g., use curl -f against the app's health endpoint like
http://localhost:8080/health) or remove the HEALTHCHECK entirely and rely on
Kubernetes liveness/readiness probes; ensure you modify the HEALTHCHECK
instruction (the HEALTHCHECK line in the Dockerfile) rather than changing
Program.cs.


# Entrypoint
ENTRYPOINT ["dotnet", "SimpleOperator.dll"]
CMD ["operator"]
19 changes: 5 additions & 14 deletions examples/SimpleOperator/Program.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
using K8sOperator.NET;
using K8sOperator.NET.Extensions;
using K8sOperator.NET.Generators;
using SimpleOperator.Projects;
using SimpleOperator.Controllers;

var builder = WebApplication.CreateBuilder(args);

var builder = OperatorHost.CreateOperatorApplicationBuilder(args)
//.WithName("simple-operator")
.WithNamespace("simple-ops-system");

builder.AddController<TestItemController>()
.WithFinalizer("testitem.local.finalizer");

builder.AddController<ProjectController>()
.WithFinalizer("project.local.finalizer");
builder.Services.AddOperator();

var app = builder.Build();

app.AddInstall();
app.MapController<TodoController>();

await app.RunAsync();
await app.RunOperatorAsync();
20 changes: 0 additions & 20 deletions examples/SimpleOperator/Projects/Project.cs

This file was deleted.

Loading
Loading