Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Introduces build tasks for code generation#90

Merged
pmdevers merged 1 commit intomainfrom
build-tasks
Jan 23, 2026
Merged

Introduces build tasks for code generation#90
pmdevers merged 1 commit intomainfrom
build-tasks

Conversation

@pmdevers
Copy link
Owner

@pmdevers pmdevers commented Jan 23, 2026

Moves code generation logic to MSBuild tasks, which improves the build process and allows for more flexibility in customizing the generated output.

Removes the commands for generating launch settings and Dockerfile, replacing them with MSBuild tasks that automatically generate these files during the build process.

Adds a configuration provider to centralize and prioritize operator configuration from assembly attributes, appsettings.json, and the OperatorBuilder, enabling easier customization.

Updates EventWatcher and commands to use the new OperatorConfiguration for settings like operator name, image, and namespace.

Adds new build tasks project and templates.

Fixes #2

Summary by CodeRabbit

  • New Features

    • Central OperatorConfiguration exposed via DI with programmatic overrides.
    • MSBuild tasks generate Dockerfile and launchSettings at build time.
    • OperatorBuilder added to configure leader-election and kube options programmatically.
  • Changes

    • Runtime now reads name/namespace/image from OperatorConfiguration.
    • Runtime generation commands removed in favor of build-time tasks; package exposes build tasks.
  • Tests

    • Added comprehensive OperatorConfiguration tests and updated existing tests to use configuration.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Centralizes operator configuration via OperatorConfiguration/provider, adds a BuildTasks project with MSBuild tasks for Dockerfile and launchSettings generation, and refactors builders, watchers, targets, commands, examples, and tests to use DI-driven configuration and task-based generation.

Changes

Cohort / File(s) Summary
Solution / BuildTasks Project
K8sOperator.NET.slnx, src/K8sOperator.NET.BuildTasks/K8sOperator.NET.BuildTasks.csproj
Adds new K8sOperator.NET.BuildTasks project to solution; netstandard2.0 project embedding templates.
MSBuild Task Implementations
src/K8sOperator.NET.BuildTasks/GenerateDockerfileTask.cs, src/K8sOperator.NET.BuildTasks/GenerateLaunchSettingsTask.cs
New MSBuild Task classes that read embedded templates and write Dockerfile, .dockerignore, and Properties/launchSettings.json if missing; include logging and error handling.
Embedded Templates
src/K8sOperator.NET.BuildTasks/Templates/*
Adds Dockerfile.template, .dockerignore.template, and launchSettings.json.template as embedded resources consumed by build tasks.
Operator.targets & Packaging
src/K8sOperator.NET/Operator.targets, src/K8sOperator.NET/K8sOperator.NET.csproj
Loads BuildTasks via UsingTask; replaces Exec-based generation with task invocations; removes embedded Docker templates from main project; adds packaging logic to include BuildTasks DLLs.
Configuration Model & Provider
src/K8sOperator.NET/Configuration/OperatorConfiguration.cs, src/K8sOperator.NET/Configuration/OperatorConfigurationProvider.cs
Introduces OperatorConfiguration (image composition, validation, defaults) and OperatorConfigurationProvider (builds config from assembly attributes and IConfiguration).
Runtime DI & Builder API
src/K8sOperator.NET/OperatorExtensions.cs, src/K8sOperator.NET/OperatorBuilder.cs
Registers OperatorConfiguration in DI via provider; adds OperatorBuilder for leader-election options; removes runtime generator command registrations from DI.
Refactor Builders & Watchers
src/K8sOperator.NET/Builder/..., src/K8sOperator.NET/EventWatcher.cs, src/K8sOperator.NET/EventWatcherDatasource.cs
Replaces List<object> metadata usage with OperatorConfiguration, propagating it through ControllerBuilder, EventWatcherBuilder, EventWatcher, and datasource.
Commands: removed / adapted
src/K8sOperator.NET/Commands/*
Removes runtime commands GenerateDockerfileCommand and GenerateLaunchSettingsCommand; refactors InstallCommand and VersionCommand to consume OperatorConfiguration.
Metadata / API Surface tweaks
src/K8sOperator.NET/Metadata/AdditionalPrinterColumn.cs
Makes Name, Type, and Path required and sets defaults for Description and Priority.
Examples & Tests
examples/SimpleOperator/*, test/K8sOperator.NET.Tests/*
Example: enabled leader election and adjusted launch profiles/registry; tests updated to use OperatorConfiguration in place of metadata lists.

Sequence Diagram(s)

sequenceDiagram
    participant MSBuild as MSBuild
    participant Project as Project (.csproj / Operator.targets)
    participant BuildTasksDll as BuildTasks.dll
    participant Task as GenerateDockerfileTask / GenerateLaunchSettingsTask
    participant Filesystem as Filesystem

    Note over MSBuild,Project: Build evaluation & task invocation
    MSBuild->>Project: Evaluate Operator.targets / _GetPackageFiles
    Project->>BuildTasksDll: Resolve _BuildTasksPath and load UsingTask types
    Project->>Task: Invoke GenerateLaunchSettingsTask(ProjectDirectory)
    Task->>BuildTasksDll: Read embedded launchSettings.json.template
    Task->>Filesystem: Write Properties/launchSettings.json (if missing)
    Project->>Task: Invoke GenerateDockerfileTask(ProjectDirectory,ProjectName,TargetFramework,OperatorName,ContainerRegistry,ContainerRepository,ContainerTag)
    Task->>BuildTasksDll: Read Dockerfile/.dockerignore templates
    Task->>Filesystem: Write Dockerfile & .dockerignore (if missing)
    Task-->>Project: Log results
Loading
sequenceDiagram
    participant Startup as App Startup
    participant DI as Dependency Injection
    participant Provider as OperatorConfigurationProvider
    participant Config as OperatorConfiguration
    participant EventDS as EventWatcherDatasource
    participant Watcher as EventWatcher

    Startup->>DI: Register services and Provider
    DI->>Provider: Instantiate (IConfiguration?, Assembly?)
    Provider->>Config: Build() (apply assembly attributes -> bind IConfiguration)
    DI-->>Config: Register singleton OperatorConfiguration
    Startup->>EventDS: Resolve (requires OperatorConfiguration)
    EventDS->>Config: Read values (Namespace, OperatorName, ContainerImage)
    EventDS->>Watcher: Create watchers passing OperatorConfiguration
    Watcher->>Config: Use Namespace / ContainerImage during API calls
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibbled templates tucked away,

Woke tasks that write at build-time play.
Config now hops through DI so neat,
Watchers and builders all align and meet.
A cheerful rabbit — code is tidy and sweet.

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Introduces build tasks for code generation' is misleading. While build tasks are added, the primary changes involve refactoring operator configuration management away from metadata-driven attributes to a centralized OperatorConfiguration object and removing two commands, which is not reflected in the title. Update title to accurately reflect the main change, such as 'Refactor operator configuration to use centralized OperatorConfiguration' or 'Move configuration from metadata attributes to OperatorConfiguration provider'.
Linked Issues check ⚠️ Warning The PR description claims it references issue #2, which is about bumping SonarAnalyzer.CSharp dependency version. However, no changes to SonarAnalyzer.CSharp package references or any dependency-related updates are present in the changeset. Either update the PR to include the actual SonarAnalyzer.CSharp version bump mentioned in issue #2, or correct the linked issues to reflect the actual scope of this PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Out of Scope Changes check ✅ Passed All changes in the PR are cohesively related to the core objective of moving code generation and configuration management from command-based and metadata-driven approaches to MSBuild tasks and a centralized OperatorConfiguration system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@examples/SimpleOperator/Properties/launchSettings.json`:
- Around line 11-14: The "Install" profile in launchSettings.json uses shell
redirection in the "commandLineArgs" value ("install > install.yaml"), but
dotnet run does not interpret '>' so the file won't be created; update the
"Install" profile's commandLineArgs to remove the redirection and either use the
CLI's supported output flag (e.g., replace the redirection with the command's
explicit output option) or leave it as just "install" and document running the
redirected command manually in a shell instead; look for the "Install" object
and its "commandLineArgs" property to make this change.

In `@src/K8sOperator.NET.BuildTasks/GenerateDockerfileTask.cs`:
- Around line 36-75: The current logic returns early if dockerfilePath exists
and always overwrites dockerignorePath when creating the Dockerfile; change
Execute (or the method containing dockerfilePath/dockerignorePath logic in
GenerateDockerfileTask) to check and handle each file individually: if
Dockerfile exists, skip creating it but continue; if .dockerignore exists, do
not overwrite it (only write if missing); use ReadEmbeddedResource and the same
placeholder replacements when creating each file; and update the Log.LogMessage
calls to report exactly which files were created (referencing dockerfilePath,
dockerignorePath, ProjectName, ExtractDotNetVersion, ReadEmbeddedResource,
ContainerRegistry/ContainerRepository/ContainerTag) instead of a single
all-or-nothing message.

In `@src/K8sOperator.NET.BuildTasks/GenerateLaunchSettingsTask.cs`:
- Around line 36-38: Replace the corrupted emoji "?" in the log message with the
intended checkmark emoji (✅) in the Log.LogMessage call that writes $"?
Generated launchSettings.json at: {launchSettingsPath}"; locate the call inside
the GenerateLaunchSettingsTask class and update the string to $"✅ Generated
launchSettings.json at: {launchSettingsPath}" so the output shows the correct
emoji alongside the launchSettingsPath variable.

In `@src/K8sOperator.NET.BuildTasks/Templates/launchSettings.json.template`:
- Around line 11-14: The launchSettings profile "Install" currently uses shell
redirection in commandLineArgs ("install > install.yaml"), which dotnet run
won't interpret; update the "Install" profile (commandName "Project") to avoid
shell redirection by replacing the argument with a CLI-supported output flag
(e.g., "install --output install.yaml") or another method the install command
provides to write its output to a file, so the generated
launchSettings.json.template no longer relies on ">" for file creation.

In `@src/K8sOperator.NET/Configuration/OperatorConfiguration.cs`:
- Around line 24-38: The ContainerImage getter currently produces invalid values
when ContainerRegistry/ContainerRepository/ContainerTag are empty; update
OperatorConfiguration so ContainerTag defaults to "latest" and the
ContainerImage property validates inputs and fails fast: if ContainerRegistry or
ContainerRepository are null/empty (or after trimming) throw an
InvalidOperationException with a clear message; keep ContainerImage as the
getter but perform the validation there (or add a Validate/EnsureValid method
called during startup) so any Dockerfile/launchSettings generation never
receives values like "ghcr.io/:". Reference ContainerTag, ContainerRegistry,
ContainerRepository, and ContainerImage (or add a Validate method on
OperatorConfiguration).
🧹 Nitpick comments (11)
test/K8sOperator.NET.Tests/EventWatcher_Tests.cs (1)

17-22: Consider removing NamespaceAttribute from test metadata.

Since EventWatcher now uses Configuration.Namespace instead of extracting namespace from metadata, the NamespaceAttribute("default") in the metadata list (line 19) is no longer used by EventWatcher. While it doesn't cause issues (defaults align), it could be misleading for future maintainers.

♻️ Remove unused attribute
     private readonly List<object> _metadata = [
             typeof(TestResource).GetCustomAttribute<KubernetesEntityAttribute>()!,
-            new NamespaceAttribute("default"),
             new LabelSelectorAttribute("label"),
             new FinalizerAttribute("finalize")
         ];
src/K8sOperator.NET.BuildTasks/GenerateLaunchSettingsTask.cs (1)

50-59: Consider extracting shared ReadEmbeddedResource helper.

This method is duplicated in GenerateDockerfileTask (lines 97-106 in the relevant snippets). Extracting it to a shared utility class would reduce duplication.

♻️ Extract to shared helper

Create a shared helper class:

// In a new file: EmbeddedResourceHelper.cs
namespace K8sOperator.NET.BuildTasks;

internal static class EmbeddedResourceHelper
{
    public static string ReadTemplate(string resourceName)
    {
        var assembly = typeof(EmbeddedResourceHelper).Assembly;
        var fullResourceName = $"K8sOperator.NET.BuildTasks.Templates.{resourceName}";

        using var stream = assembly.GetManifestResourceStream(fullResourceName)
            ?? throw new InvalidOperationException($"Could not find embedded resource: {fullResourceName}");
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
}

Then update both tasks to use it:

-    private static string ReadEmbeddedResource(string resourceName)
-    {
-        var assembly = typeof(GenerateLaunchSettingsTask).Assembly;
-        var fullResourceName = $"K8sOperator.NET.BuildTasks.Templates.{resourceName}";
-
-        using var stream = assembly.GetManifestResourceStream(fullResourceName)
-            ?? throw new InvalidOperationException($"Could not find embedded resource: {fullResourceName}");
-        using var reader = new StreamReader(stream);
-        return reader.ReadToEnd();
-    }
+    // Use: EmbeddedResourceHelper.ReadTemplate("launchSettings.json.template")
src/K8sOperator.NET.BuildTasks/K8sOperator.NET.BuildTasks.csproj (1)

11-14: Consider pinning package versions to specific releases for reproducibility.

Using floating versions 17.12.* for MSBuild packages can lead to non-reproducible builds if a patch release introduces breaking changes or regressions. Pin to the latest patch version in the 17.12.x line (17.12.50).

♻️ Pin to specific versions
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build.Framework" Version="17.12.*" />
-    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.*" />
+    <PackageReference Include="Microsoft.Build.Framework" Version="17.12.50" />
+    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.50" />
   </ItemGroup>
test/K8sOperator.NET.Tests/EventWatcherDatasource_Tests.cs (1)

33-47: Rename “metadata” identifiers/test names to “configuration” for clarity.

The tests now use OperatorConfiguration, but several locals/test names still say “metadata,” which is slightly misleading. Consider renaming to match the new type.

✏️ Example cleanup (apply similarly elsewhere)
-    public async Task Constructor_Should_InitializeWithServiceProviderAndMetadata()
+    public async Task Constructor_Should_InitializeWithServiceProviderAndConfiguration()
     {
         // Arrange
         var serviceProvider = CreateServiceProvider();
-        var metadata = CreateConfiguration();
+        var configuration = CreateConfiguration();

         // Act
-        var datasource = new EventWatcherDatasource(serviceProvider, metadata);
+        var datasource = new EventWatcherDatasource(serviceProvider, configuration);

         // Assert
         await Assert.That(datasource).IsNotNull();
         await Assert.That(datasource.ServiceProvider).IsEqualTo(serviceProvider);
-        await Assert.That(datasource.Configuration).IsEqualTo(metadata);
+        await Assert.That(datasource.Configuration).IsEqualTo(configuration);
     }
src/K8sOperator.NET/K8sOperator.NET.csproj (2)

40-44: Align Microsoft.Extensions.Configuration.Binder patch level with the other Microsoft.Extensions.* packages.

This project references 10.0.2 for other Microsoft.Extensions.* packages (DependencyInjection, Hosting.Abstractions), but Binder is pinned to 10.0.0. Update it to 10.0.2 to maintain consistency across the dependency graph and avoid potential downgrade warnings.

Suggested change
-    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />

58-65: Consider moving PackBuildTasks to run before Pack to ensure robustness with alternative packaging workflows.

The current AfterTargets="Build" approach works with the standard workflow (dotnet build auto-packs via GeneratePackageOnBuild=true). However, if the packaging process ever uses dotnet pack --no-build, the Build step is skipped, PackBuildTasks never runs, and the BuildTasks DLL won't be included. Using BeforeTargets="Pack" instead would ensure the target runs before Pack regardless of which command invoked it, making the packaging more resilient to different build workflows.

src/K8sOperator.NET/Configuration/OperatorConfigurationProvider.cs (1)

25-38: Consider adding validation for required configuration properties.

The Build() method returns configuration without validating that essential properties like OperatorName or ContainerRepository are set. If these remain empty, downstream components (e.g., ContainerImage computation) may produce invalid values like ghcr.io/:.

💡 Suggested validation
     public OperatorConfiguration Build()
     {
         var config = new OperatorConfiguration();

         // 1. Start with assembly attributes (lowest priority)
         ApplyAssemblyAttributes(config);

         // 2. Apply configuration (e.g., appsettings.json)
         ApplyConfiguration(config);

         // 3. OperatorBuilder can override in AddOperator() (highest priority, done by caller)

+        // Validate required properties
+        if (string.IsNullOrEmpty(config.OperatorName))
+        {
+            throw new InvalidOperationException("OperatorName must be configured via assembly attributes, appsettings.json, or OperatorBuilder.");
+        }
+
         return config;
     }
src/K8sOperator.NET/OperatorExtensions.cs (1)

24-47: Override logic may not work as intended for properties with defaults.

The checks !string.IsNullOrEmpty(builder.OperatorConfiguration.X) prevent overriding, but OperatorConfiguration.Namespace defaults to "default" and ContainerRegistry defaults to "ghcr.io". If a user explicitly sets Namespace = "default" in OperatorBuilder, it would be applied. However, the intent seems to be "apply if user explicitly configured it" — currently there's no way to distinguish between "user set empty" vs "user didn't set anything."

Consider using a separate flag or nullable properties in OperatorBuilder.OperatorConfiguration to track explicit user intent, or document that empty strings in builder config mean "don't override."

src/K8sOperator.NET/Operator.targets (3)

3-11: BuildTasks path resolution could be more robust.

The hardcoded paths assume netstandard2.0 and specific build configurations. If the target framework changes or a custom configuration is used, the task won't be found.

💡 Consider using MSBuild properties for flexibility
 <PropertyGroup>
-    <_BuildTasksPath Condition="'$(_BuildTasksPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Debug\netstandard2.0\K8sOperator.NET.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Debug\netstandard2.0\K8sOperator.NET.BuildTasks.dll</_BuildTasksPath>
-    <_BuildTasksPath Condition="'$(_BuildTasksPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Release\netstandard2.0\K8sOperator.NET.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Release\netstandard2.0\K8sOperator.NET.BuildTasks.dll</_BuildTasksPath>
+    <_BuildTasksPath Condition="'$(_BuildTasksPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\$(Configuration)\netstandard2.0\K8sOperator.NET.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\$(Configuration)\netstandard2.0\K8sOperator.NET.BuildTasks.dll</_BuildTasksPath>
     <_BuildTasksPath Condition="'$(_BuildTasksPath)' == ''">$(MSBuildThisFileDirectory)..\build\netstandard2.0\K8sOperator.NET.BuildTasks.dll</_BuildTasksPath>
 </PropertyGroup>

88-89: Inconsistent indentation on target element.

Line 88 has extra leading whitespace compared to other target definitions.

🧹 Fix indentation
-		<!-- Generate Dockerfile if it doesn't exist -->
-	<Target Name="GenerateDockerfile" BeforeTargets="Build" Condition=" '$(GenerateOperatorDockerfile)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\Dockerfile') ">
+	<!-- Generate Dockerfile if it doesn't exist -->
+	<Target Name="GenerateDockerfile" BeforeTargets="Build" Condition=" '$(GenerateOperatorDockerfile)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\Dockerfile') ">

100-103: Remove trailing blank lines.

The file has unnecessary blank lines at the end after the closing </Project> tag.

Comment on lines 11 to 14
"Install": {
"commandName": "Project",
"commandLineArgs": "install",
"commandLineArgs": "install > install.yaml",
"environmentVariables": {
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

Shell redirection won’t work in launchSettings.

commandLineArgs is passed directly to dotnet run; the > redirection isn’t interpreted, so install.yaml won’t be created. Prefer a supported output flag in the command or keep redirection as a manual shell step.

🔧 Suggested fix (avoid redirection in launchSettings)
-      "commandLineArgs": "install > install.yaml",
+      "commandLineArgs": "install",
📝 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
"Install": {
"commandName": "Project",
"commandLineArgs": "install",
"commandLineArgs": "install > install.yaml",
"environmentVariables": {
"Install": {
"commandName": "Project",
"commandLineArgs": "install",
"environmentVariables": {
🤖 Prompt for AI Agents
In `@examples/SimpleOperator/Properties/launchSettings.json` around lines 11 - 14,
The "Install" profile in launchSettings.json uses shell redirection in the
"commandLineArgs" value ("install > install.yaml"), but dotnet run does not
interpret '>' so the file won't be created; update the "Install" profile's
commandLineArgs to remove the redirection and either use the CLI's supported
output flag (e.g., replace the redirection with the command's explicit output
option) or leave it as just "install" and document running the redirected
command manually in a shell instead; look for the "Install" object and its
"commandLineArgs" property to make this change.

Comment on lines 36 to 38
Log.LogMessage(MessageImportance.High, "");
Log.LogMessage(MessageImportance.High, $"? Generated launchSettings.json at: {launchSettingsPath}");
Log.LogMessage(MessageImportance.High, $" Profiles: Operator, Install, Version, Help");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix corrupted emoji character in log message.

Line 37 contains ? which appears to be a corrupted emoji character (likely intended to be ✅).

🐛 Fix the character
             // Log success
             Log.LogMessage(MessageImportance.High, "");
-            Log.LogMessage(MessageImportance.High, $"? Generated launchSettings.json at: {launchSettingsPath}");
+            Log.LogMessage(MessageImportance.High, $"✅ Generated launchSettings.json at: {launchSettingsPath}");
             Log.LogMessage(MessageImportance.High, $"   Profiles: Operator, Install, Version, Help");
📝 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
Log.LogMessage(MessageImportance.High, "");
Log.LogMessage(MessageImportance.High, $"? Generated launchSettings.json at: {launchSettingsPath}");
Log.LogMessage(MessageImportance.High, $" Profiles: Operator, Install, Version, Help");
Log.LogMessage(MessageImportance.High, "");
Log.LogMessage(MessageImportance.High, $" Generated launchSettings.json at: {launchSettingsPath}");
Log.LogMessage(MessageImportance.High, $" Profiles: Operator, Install, Version, Help");
🤖 Prompt for AI Agents
In `@src/K8sOperator.NET.BuildTasks/GenerateLaunchSettingsTask.cs` around lines 36
- 38, Replace the corrupted emoji "?" in the log message with the intended
checkmark emoji (✅) in the Log.LogMessage call that writes $"? Generated
launchSettings.json at: {launchSettingsPath}"; locate the call inside the
GenerateLaunchSettingsTask class and update the string to $"✅ Generated
launchSettings.json at: {launchSettingsPath}" so the output shows the correct
emoji alongside the launchSettingsPath variable.

Comment on lines +11 to +14
"Install": {
"commandName": "Project",
"commandLineArgs": "install > install.yaml",
"environmentVariables": {
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

Shell redirection won’t work in generated launchSettings.

The task writes this into launchSettings, but dotnet run won’t interpret >; the file won’t be produced.

🔧 Suggested fix (avoid redirection in template)
-      "commandLineArgs": "install > install.yaml",
+      "commandLineArgs": "install",
📝 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
"Install": {
"commandName": "Project",
"commandLineArgs": "install > install.yaml",
"environmentVariables": {
"Install": {
"commandName": "Project",
"commandLineArgs": "install",
"environmentVariables": {
🤖 Prompt for AI Agents
In `@src/K8sOperator.NET.BuildTasks/Templates/launchSettings.json.template` around
lines 11 - 14, The launchSettings profile "Install" currently uses shell
redirection in commandLineArgs ("install > install.yaml"), which dotnet run
won't interpret; update the "Install" profile (commandName "Project") to avoid
shell redirection by replacing the argument with a CLI-supported output flag
(e.g., "install --output install.yaml") or another method the install command
provides to write its output to a file, so the generated
launchSettings.json.template no longer relies on ">" for file creation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/K8sOperator.NET/Commands/VersionCommand.cs`:
- Around line 18-25: The current VersionCommand code guards on
config.OperatorName and config.ContainerTag but then accesses
config.ContainerImage which can throw InvalidOperationException if
ContainerRepository is not set; update VersionCommand (the method that prints
operator/version info) to call config.Validate() before accessing ContainerImage
(or wrap the Console.WriteLine that references config.ContainerImage in a
try/catch that catches InvalidOperationException and prints a friendly message),
and remove the redundant ContainerTag null/whitespace check since ContainerTag
defaults to "latest"; reference the config.Validate() method and the properties
ContainerImage, ContainerRepository, and ContainerTag when making the change.

In `@src/K8sOperator.NET/Configuration/OperatorConfigurationProvider.cs`:
- Around line 71-82: ApplyConfiguration currently binds the entire "Operator"
section into the OperatorConfiguration instance with section.Bind(config), which
can overwrite existing assembly/default values with empty strings; change this
to selectively apply only non-empty values (e.g., use
configuration.GetSection("Operator").GetValue<string>("OperatorName") and set
config.OperatorName only when value is not null/empty) or perform post-bind
validation to restore defaults for any empty properties. Update the
ApplyConfiguration method to check each relevant property after reading the
section (or replace Bind with per-key GetValue calls) and only assign to
OperatorConfiguration fields when the config value is non-empty; keep references
to ApplyConfiguration, OperatorConfiguration,
configuration.GetSection("Operator") and section.Bind(config) to locate where to
modify.

In `@src/K8sOperator.NET/Operator.targets`:
- Around line 88-98: The GenerateDockerfile target can run before
$(_FullContainerRepository) and $(_FullContainerTag) are computed; update the
MSBuild ordering so GenerateDockerfile runs after/Adds a dependency on
AddDockerImageAttribute (the target that computes those properties) to guarantee
the properties are populated before invoking GenerateDockerfileTask.
Specifically, ensure the Target named "GenerateDockerfile" depends on or runs
after the "AddDockerImageAttribute" target so $(_FullContainerRepository) and
$(_FullContainerTag) are set when GenerateDockerfileTask executes.

In `@test/K8sOperator.NET.Tests/OperatorConfiguration_Tests.cs`:
- Around line 124-128: The test captures an exception from Assert.That(() =>
config.ContainerImage).Throws<InvalidOperationException>() into the variable
exception but then dereferences exception.Message without guarding against a
null return from Throws<>; add a null check (e.g., Assert.NotNull(exception) or
use null-conditional access) before asserting on Message to satisfy the static
analyzer. Apply the same fix to the other test blocks that capture exceptions
(the other occurrences using the local variable exception at the ranges
corresponding to the ContainerImage/other property tests) so each assertion on
exception.Message is guarded.
♻️ Duplicate comments (3)
src/K8sOperator.NET.BuildTasks/GenerateDockerfileTask.cs (1)

39-59: Existing .dockerignore may be overwritten.

The task skips generation only if Dockerfile exists, but always writes .dockerignore when creating a new Dockerfile. This can wipe user customizations to .dockerignore.

Consider checking each file independently:

🛠️ Suggested fix
-            if (File.Exists(dockerfilePath))
+            var dockerfileExists = File.Exists(dockerfilePath);
+            var dockerignoreExists = File.Exists(dockerignorePath);
+
+            if (dockerfileExists)
             {
                 Log.LogMessage(MessageImportance.Normal, $"Dockerfile already exists at {dockerfilePath}, skipping generation");
-                return true;
+                if (dockerignoreExists)
+                    return true;
             }

             // Extract .NET version from TargetFramework
             var dotnetVersion = ExtractDotNetVersion(TargetFramework);

-            // Read templates from embedded resources
-            var dockerfileContent = ReadEmbeddedResource("Dockerfile.template");
-            var dockerignoreContent = ReadEmbeddedResource(".dockerignore.template");
-
-            // Replace placeholders
-            dockerfileContent = dockerfileContent
-                .Replace("{PROJECT_NAME}", ProjectName)
-                .Replace("{DOTNET_VERSION}", dotnetVersion);
-
-            // Write files
-            File.WriteAllText(dockerfilePath, dockerfileContent);
-            File.WriteAllText(dockerignorePath, dockerignoreContent);
+            if (!dockerfileExists)
+            {
+                var dockerfileContent = ReadEmbeddedResource("Dockerfile.template")
+                    .Replace("{PROJECT_NAME}", ProjectName)
+                    .Replace("{DOTNET_VERSION}", dotnetVersion);
+                File.WriteAllText(dockerfilePath, dockerfileContent);
+                Log.LogMessage(MessageImportance.High, $"✅ Generated Dockerfile at: {dockerfilePath}");
+            }
+
+            if (!dockerignoreExists)
+            {
+                var dockerignoreContent = ReadEmbeddedResource(".dockerignore.template");
+                File.WriteAllText(dockerignorePath, dockerignoreContent);
+                Log.LogMessage(MessageImportance.High, $"✅ Generated .dockerignore at: {dockerignorePath}");
+            }
src/K8sOperator.NET.BuildTasks/Templates/launchSettings.json.template (1)

11-18: Shell redirection in commandLineArgs won't work.

The > install.yaml redirection won't be interpreted by dotnet run; the output file won't be created. Consider using a CLI flag if the install command supports it (e.g., --output install.yaml), or remove the redirection and let users handle output manually.

🛠️ Suggested fix
     "Install": {
       "commandName": "Project",
-      "commandLineArgs": "install > install.yaml",
+      "commandLineArgs": "install",
       "environmentVariables": {
src/K8sOperator.NET.BuildTasks/GenerateLaunchSettingsTask.cs (1)

36-38: Fix corrupted emoji character in log message.

This issue was previously identified - line 37 contains ? which should be .

🧹 Nitpick comments (11)
src/K8sOperator.NET.BuildTasks/K8sOperator.NET.BuildTasks.csproj (1)

11-14: Consider pinning exact versions for reproducible builds.

Using wildcard versions (17.12.*) for MSBuild packages can lead to non-reproducible builds. Consider pinning to exact versions:

-    <PackageReference Include="Microsoft.Build.Framework" Version="17.12.*" />
-    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.*" />
+    <PackageReference Include="Microsoft.Build.Framework" Version="17.12.0" />
+    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.0" />
test/K8sOperator.NET.Tests/EventWatcher_Tests.cs (1)

34-107: Consider extracting a helper method for test configuration.

Multiple tests instantiate new OperatorConfiguration(). For consistency with EventWatcherDatasource_Tests.CreateConfiguration() and easier maintenance if defaults need to change:

private static OperatorConfiguration CreateConfiguration() => new();

Then use CreateConfiguration() in each test. This is optional since the current approach works correctly.

src/K8sOperator.NET/Commands/InstallCommand.cs (1)

95-151: Consider adding config validation before generating manifests.

If OperatorConfiguration has invalid values (empty OperatorName, missing ContainerImage components), the generated manifests will be invalid. Consider calling config.Validate() at the start of RunAsync to fail fast with a clear error message rather than generating broken YAML.

 public async Task RunAsync(string[] args)
 {
     var dataSource = app.Services.GetRequiredService<EventWatcherDatasource>();
     var config = app.Services.GetRequiredService<OperatorConfiguration>();
+    config.Validate();
     var watchers = dataSource.GetWatchers().ToList();
test/K8sOperator.NET.Tests/OperatorConfiguration_Tests.cs (1)

274-289: Async test method without await.

This test is marked async but doesn't await anything. Either remove the async keyword or add an assertion that uses await.

♻️ Suggested fix
     [Test]
-    public async Task Validate_Should_NotThrow_WhenAllPropertiesAreValid()
+    public Task Validate_Should_NotThrow_WhenAllPropertiesAreValid()
     {
         // Arrange
         var config = new OperatorConfiguration
         {
             OperatorName = "my-operator",
             Namespace = "default",
             ContainerRegistry = "ghcr.io",
             ContainerRepository = "myorg/myapp",
             ContainerTag = "1.0.0"
         };

         // Act & Assert - Should not throw
         config.Validate();
+        return Task.CompletedTask;
     }
src/K8sOperator.NET.BuildTasks/GenerateLaunchSettingsTask.cs (1)

50-59: Consider extracting shared ReadEmbeddedResource helper to reduce duplication.

This method is identical to the one in GenerateDockerfileTask.cs. Consider extracting it to a shared utility class to follow DRY principles.

♻️ Example extraction
// In a new file: EmbeddedResourceHelper.cs
internal static class EmbeddedResourceHelper
{
    public static string ReadEmbeddedResource(Type anchorType, string resourceName)
    {
        var assembly = anchorType.Assembly;
        var fullResourceName = $"K8sOperator.NET.BuildTasks.Templates.{resourceName}";

        using var stream = assembly.GetManifestResourceStream(fullResourceName)
            ?? throw new InvalidOperationException($"Could not find embedded resource: {fullResourceName}");
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
}
src/K8sOperator.NET/Builder/ControllerBuilder.cs (1)

34-37: Remove trailing blank lines.

Minor formatting issue - there are extra blank lines at the end of the file.

src/K8sOperator.NET/K8sOperator.NET.csproj (1)

58-65: Consider using the official NuGet pack extension point TargetsForTfmSpecificContentInPackage instead of a custom target.

While the PackBuildTasks target with AfterTargets="Build" should work correctly (since it runs before the pack phase and the Pack metadata is evaluated during GenerateNuspec), the recommended approach for including additional files in a NuGet package is to use the official extension point TargetsForTfmSpecificContentInPackage. This provides better integration with NuGet's build process and is more maintainable.

src/K8sOperator.NET/EventWatcherDatasource.cs (1)

62-63: Remove trailing blank lines.

Unnecessary trailing blank lines at end of file.

test/K8sOperator.NET.Tests/EventWatcherDatasource_Tests.cs (2)

33-47: Consider renaming local variable for clarity.

The local variable metadata holds an OperatorConfiguration instance. Renaming it to configuration would improve readability and align with the property name being asserted.

Suggested variable rename
     public async Task Constructor_Should_InitializeWithServiceProviderAndMetadata()
     {
         // Arrange
         var serviceProvider = CreateServiceProvider();
-        var metadata = CreateConfiguration();
+        var configuration = CreateConfiguration();

         // Act
-        var datasource = new EventWatcherDatasource(serviceProvider, metadata);
+        var datasource = new EventWatcherDatasource(serviceProvider, configuration);

         // Assert
         await Assert.That(datasource).IsNotNull();
         await Assert.That(datasource.ServiceProvider).IsEqualTo(serviceProvider);
-        await Assert.That(datasource.Configuration).IsEqualTo(metadata);
+        await Assert.That(datasource.Configuration).IsEqualTo(configuration);
     }

272-282: Test name no longer matches what it tests.

The test Metadata_Should_BeAccessibleProperty now tests Configuration accessibility. Consider renaming for clarity.

Suggested rename
     [Test]
-    public async Task Metadata_Should_BeAccessibleProperty()
+    public async Task Configuration_Should_BeAccessibleProperty()
     {
src/K8sOperator.NET/Operator.targets (1)

100-103: Remove trailing blank lines.

Unnecessary trailing blank lines at end of file.

Comment on lines +18 to +25
if (string.IsNullOrWhiteSpace(config.OperatorName) || string.IsNullOrWhiteSpace(config.ContainerTag))
{
Console.WriteLine("Operator name or version metadata is missing.");
return Task.CompletedTask;
}

Console.WriteLine($"{name} version {version}.");
Console.WriteLine($"Docker Info: {version.GetImage()}.");
Console.WriteLine($"{config.OperatorName} version {config.ContainerTag}.");
Console.WriteLine($"Docker Info: {config.ContainerImage}.");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Accessing ContainerImage may throw if ContainerRepository is not configured.

config.ContainerTag will never be null/whitespace (defaults to "latest"), so the check at line 18 is partially redundant. However, accessing config.ContainerImage at line 25 will throw InvalidOperationException if ContainerRepository is empty.

Consider either:

  1. Calling config.Validate() before accessing properties, or
  2. Wrapping in try-catch with a user-friendly message
🛠️ Suggested fix
     public Task RunAsync(string[] args)
     {
         var config = app.Services.GetRequiredService<OperatorConfiguration>();

-        if (string.IsNullOrWhiteSpace(config.OperatorName) || string.IsNullOrWhiteSpace(config.ContainerTag))
+        if (string.IsNullOrWhiteSpace(config.OperatorName))
         {
-            Console.WriteLine("Operator name or version metadata is missing.");
+            Console.WriteLine("Operator name metadata is missing.");
             return Task.CompletedTask;
         }

         Console.WriteLine($"{config.OperatorName} version {config.ContainerTag}.");
-        Console.WriteLine($"Docker Info: {config.ContainerImage}.");
+        try
+        {
+            Console.WriteLine($"Docker Info: {config.ContainerImage}.");
+        }
+        catch (InvalidOperationException)
+        {
+            Console.WriteLine("Docker Info: Not fully configured.");
+        }
         return Task.CompletedTask;
     }
🤖 Prompt for AI Agents
In `@src/K8sOperator.NET/Commands/VersionCommand.cs` around lines 18 - 25, The
current VersionCommand code guards on config.OperatorName and
config.ContainerTag but then accesses config.ContainerImage which can throw
InvalidOperationException if ContainerRepository is not set; update
VersionCommand (the method that prints operator/version info) to call
config.Validate() before accessing ContainerImage (or wrap the Console.WriteLine
that references config.ContainerImage in a try/catch that catches
InvalidOperationException and prints a friendly message), and remove the
redundant ContainerTag null/whitespace check since ContainerTag defaults to
"latest"; reference the config.Validate() method and the properties
ContainerImage, ContainerRepository, and ContainerTag when making the change.

Comment on lines +71 to +82
private void ApplyConfiguration(OperatorConfiguration config)
{
if (configuration == null)
return;

// Bind from appsettings.json section: "Operator"
var section = configuration.GetSection("Operator");
if (section.Exists())
{
section.Bind(config);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential issue: Bind() may overwrite values with empty strings from config.

If the "Operator" section exists in appsettings.json with empty string values (e.g., "OperatorName": ""), Bind() will overwrite the assembly attribute values with empty strings. Consider using a more selective binding approach or adding validation after binding.

Alternative approach using individual value checks
     private void ApplyConfiguration(OperatorConfiguration config)
     {
         if (configuration == null)
             return;

         // Bind from appsettings.json section: "Operator"
         var section = configuration.GetSection("Operator");
         if (section.Exists())
         {
-            section.Bind(config);
+            var operatorName = section["OperatorName"];
+            if (!string.IsNullOrEmpty(operatorName))
+                config.OperatorName = operatorName;
+
+            var ns = section["Namespace"];
+            if (!string.IsNullOrEmpty(ns))
+                config.Namespace = ns;
+
+            var registry = section["ContainerRegistry"];
+            if (!string.IsNullOrEmpty(registry))
+                config.ContainerRegistry = registry;
+
+            var repository = section["ContainerRepository"];
+            if (!string.IsNullOrEmpty(repository))
+                config.ContainerRepository = repository;
+
+            var tag = section["ContainerTag"];
+            if (!string.IsNullOrEmpty(tag))
+                config.ContainerTag = tag;
         }
     }
📝 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
private void ApplyConfiguration(OperatorConfiguration config)
{
if (configuration == null)
return;
// Bind from appsettings.json section: "Operator"
var section = configuration.GetSection("Operator");
if (section.Exists())
{
section.Bind(config);
}
}
private void ApplyConfiguration(OperatorConfiguration config)
{
if (configuration == null)
return;
// Bind from appsettings.json section: "Operator"
var section = configuration.GetSection("Operator");
if (section.Exists())
{
var operatorName = section["OperatorName"];
if (!string.IsNullOrEmpty(operatorName))
config.OperatorName = operatorName;
var ns = section["Namespace"];
if (!string.IsNullOrEmpty(ns))
config.Namespace = ns;
var registry = section["ContainerRegistry"];
if (!string.IsNullOrEmpty(registry))
config.ContainerRegistry = registry;
var repository = section["ContainerRepository"];
if (!string.IsNullOrEmpty(repository))
config.ContainerRepository = repository;
var tag = section["ContainerTag"];
if (!string.IsNullOrEmpty(tag))
config.ContainerTag = tag;
}
}
🤖 Prompt for AI Agents
In `@src/K8sOperator.NET/Configuration/OperatorConfigurationProvider.cs` around
lines 71 - 82, ApplyConfiguration currently binds the entire "Operator" section
into the OperatorConfiguration instance with section.Bind(config), which can
overwrite existing assembly/default values with empty strings; change this to
selectively apply only non-empty values (e.g., use
configuration.GetSection("Operator").GetValue<string>("OperatorName") and set
config.OperatorName only when value is not null/empty) or perform post-bind
validation to restore defaults for any empty properties. Update the
ApplyConfiguration method to check each relevant property after reading the
section (or replace Bind with per-key GetValue calls) and only assign to
OperatorConfiguration fields when the config value is non-empty; keep references
to ApplyConfiguration, OperatorConfiguration,
configuration.GetSection("Operator") and section.Bind(config) to locate where to
modify.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/K8sOperator.NET.Tests/OperatorConfiguration_Tests.cs`:
- Around line 283-297: The test method
Validate_Should_NotThrow_WhenAllPropertiesAreValid is declared async but
contains no awaits; remove the unnecessary async and change the signature to a
synchronous test (e.g., "public void
Validate_Should_NotThrow_WhenAllPropertiesAreValid()") in the
OperatorConfiguration_Tests class so you no longer get CS1998 warnings.
♻️ Duplicate comments (3)
src/K8sOperator.NET/Commands/VersionCommand.cs (1)

16-25: Guard ContainerImage access or validate config first.

ContainerImage can throw if ContainerRegistry/ContainerRepository is empty, and the ContainerTag check is redundant because it defaults to "latest". Consider calling config.Validate() (or catching InvalidOperationException) before printing to avoid unexpected crashes.

src/K8sOperator.NET/Configuration/OperatorConfigurationProvider.cs (1)

71-81: Bind() can clobber defaults with empty strings.

If appsettings.json contains empty values, Bind overwrites assembly/default values. Consider applying only non-empty values.

🛠️ Suggested selective binding
-        if (section.Exists())
-        {
-            section.Bind(config);
-        }
+        if (section.Exists())
+        {
+            var operatorName = section["OperatorName"];
+            if (!string.IsNullOrWhiteSpace(operatorName))
+                config.OperatorName = operatorName;
+
+            var ns = section["Namespace"];
+            if (!string.IsNullOrWhiteSpace(ns))
+                config.Namespace = ns;
+
+            var registry = section["ContainerRegistry"];
+            if (!string.IsNullOrWhiteSpace(registry))
+                config.ContainerRegistry = registry;
+
+            var repository = section["ContainerRepository"];
+            if (!string.IsNullOrWhiteSpace(repository))
+                config.ContainerRepository = repository;
+
+            var tag = section["ContainerTag"];
+            if (!string.IsNullOrWhiteSpace(tag))
+                config.ContainerTag = tag;
+        }
src/K8sOperator.NET/Operator.targets (1)

88-97: GenerateDockerfile may run before image properties are computed.

_FullContainerRepository/_FullContainerTag are built in AddDockerImageAttribute, but GenerateDockerfile runs before Build, so the task can see empty values. Ensure GenerateDockerfile depends on AddDockerImageAttribute.

🛠️ Add dependency to guarantee ordering
-	<Target Name="GenerateDockerfile" BeforeTargets="Build" Condition=" '$(GenerateOperatorDockerfile)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\Dockerfile') ">
+	<Target Name="GenerateDockerfile" BeforeTargets="Build" DependsOnTargets="AddDockerImageAttribute" Condition=" '$(GenerateOperatorDockerfile)' == 'true' AND !Exists('$(MSBuildProjectDirectory)\Dockerfile') ">
🧹 Nitpick comments (5)
src/K8sOperator.NET.BuildTasks/GenerateLaunchSettingsTask.cs (1)

50-59: Consider extracting shared ReadEmbeddedResource helper.

This method is duplicated in GenerateDockerfileTask.cs (lines 100-109). Consider extracting it to a shared utility class to reduce duplication.

♻️ Suggested extraction
// In a new file: EmbeddedResourceHelper.cs
namespace K8sOperator.NET.BuildTasks;

internal static class EmbeddedResourceHelper
{
    public static string ReadEmbeddedResource(string resourceName)
    {
        var assembly = typeof(EmbeddedResourceHelper).Assembly;
        var fullResourceName = $"K8sOperator.NET.BuildTasks.Templates.{resourceName}";

        using var stream = assembly.GetManifestResourceStream(fullResourceName)
            ?? throw new InvalidOperationException($"Could not find embedded resource: {fullResourceName}");
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
}
src/K8sOperator.NET.BuildTasks/GenerateDockerfileTask.cs (1)

66-76: Consider logging build instructions only when files are generated.

The operator info and docker build/push instructions are logged on every build, even when both files already exist. This could create unnecessary noise in build output. Consider making this logging conditional.

♻️ Suggested improvement
+            var anyFileGenerated = false;
+
             if (!File.Exists(dockerfilePath))
             {
                 File.WriteAllText(dockerfilePath, dockerfileContent);
-
                 Log.LogMessage(MessageImportance.High, $"Generated Dockerfile at: {dockerfilePath}");
-
+                anyFileGenerated = true;
             }

             if (!File.Exists(dockerignorePath))
             {
                 File.WriteAllText(dockerignorePath, dockerignoreContent);
-
                 Log.LogMessage(MessageImportance.High, $"Generated .dockerignore at: {dockerignorePath}");
+                anyFileGenerated = true;
             }

-            // Log success
-            Log.LogMessage(MessageImportance.High, $"Operator: {OperatorName}");
-            Log.LogMessage(MessageImportance.High, $"  .NET Version: {dotnetVersion}");
-            Log.LogMessage(MessageImportance.High, $"   Image: {ContainerRegistry}/{ContainerRepository}:{ContainerTag}");
-            Log.LogMessage(MessageImportance.High, "");
-            Log.LogMessage(MessageImportance.High, "To build the image:");
-            Log.LogMessage(MessageImportance.High, $"  docker build -t {ContainerRegistry}/{ContainerRepository}:{ContainerTag} .");
-            Log.LogMessage(MessageImportance.High, "");
-            Log.LogMessage(MessageImportance.High, "To push the image:");
-            Log.LogMessage(MessageImportance.High, $"  docker push {ContainerRegistry}/{ContainerRepository}:{ContainerTag}");
-            Log.LogMessage(MessageImportance.High, "");
+            if (anyFileGenerated)
+            {
+                Log.LogMessage(MessageImportance.High, $"Operator: {OperatorName}");
+                Log.LogMessage(MessageImportance.High, $"  .NET Version: {dotnetVersion}");
+                Log.LogMessage(MessageImportance.High, $"   Image: {ContainerRegistry}/{ContainerRepository}:{ContainerTag}");
+                Log.LogMessage(MessageImportance.High, "");
+                Log.LogMessage(MessageImportance.High, "To build the image:");
+                Log.LogMessage(MessageImportance.High, $"  docker build -t {ContainerRegistry}/{ContainerRepository}:{ContainerTag} .");
+                Log.LogMessage(MessageImportance.High, "");
+                Log.LogMessage(MessageImportance.High, "To push the image:");
+                Log.LogMessage(MessageImportance.High, $"  docker push {ContainerRegistry}/{ContainerRepository}:{ContainerTag}");
+                Log.LogMessage(MessageImportance.High, "");
+            }
test/K8sOperator.NET.Tests/EventWatcherDatasource_Tests.cs (1)

33-47: Rename metadata locals to configuration for clarity.

These variables now hold OperatorConfiguration, so the current name is misleading. Consider renaming across the test file for readability.

src/K8sOperator.NET/Commands/InstallCommand.cs (1)

18-27: Validate configuration before manifest generation.

CreateDeployment accesses config.ContainerImage, which throws if registry/repository are missing. Consider validating up-front and printing a friendly message before proceeding.

♻️ Suggested change
 public async Task RunAsync(string[] args)
 {
     var dataSource = app.Services.GetRequiredService<EventWatcherDatasource>();
     var config = app.Services.GetRequiredService<OperatorConfiguration>();
+    try
+    {
+        config.Validate();
+    }
+    catch (InvalidOperationException ex)
+    {
+        Console.WriteLine(ex.Message);
+        return;
+    }
     var watchers = dataSource.GetWatchers().ToList();
     var ns = CreateNamespace(config);
     var clusterrole = CreateClusterRole(config, watchers);
     var clusterrolebinding = CreateClusterRoleBinding(config);
     var deployment = CreateDeployment(config);
src/K8sOperator.NET/OperatorExtensions.cs (1)

24-47: Consider validating OperatorConfiguration before returning.

Fail fast on missing required settings instead of deferring to later runtime paths.

♻️ Proposed tweak
-                return config;
+                config.Validate();
+                return config;

Comment on lines 283 to 297
public async Task Validate_Should_NotThrow_WhenAllPropertiesAreValid()
{
// Arrange
var config = new OperatorConfiguration
{
OperatorName = "my-operator",
Namespace = "default",
ContainerRegistry = "ghcr.io",
ContainerRepository = "myorg/myapp",
ContainerTag = "1.0.0"
};

// Act & Assert - Should not throw
config.Validate();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the test file to confirm the code
find . -name "OperatorConfiguration_Tests.cs" -type f

Repository: pmdevers/K8sOperator.NET

Length of output: 127


🏁 Script executed:

# Search for project files and warning configuration
fd -e csproj -o fd -e editorconfig | head -20

Repository: pmdevers/K8sOperator.NET

Length of output: 191


🏁 Script executed:

# Check for warning configuration in csproj files
rg -n "TreatWarningsAsErrors|WarningsAsErrors|CS1998" -g "*.csproj" -g "*.editorconfig"

Repository: pmdevers/K8sOperator.NET

Length of output: 50


🏁 Script executed:

# Read the actual test file to confirm the code
cat -n ./test/K8sOperator.NET.Tests/OperatorConfiguration_Tests.cs | sed -n '280,300p'

Repository: pmdevers/K8sOperator.NET

Length of output: 775


🏁 Script executed:

# Find csproj files
find . -name "*.csproj" -type f

Repository: pmdevers/K8sOperator.NET

Length of output: 357


🏁 Script executed:

# Search for warning configuration in all csproj and editorconfig files
find . \( -name "*.csproj" -o -name ".editorconfig" \) -type f -exec grep -l "TreatWarningsAsErrors\|WarningsAsErrors\|CS1998" {} \;

Repository: pmdevers/K8sOperator.NET

Length of output: 50


🏁 Script executed:

# Read the test project csproj to check warning settings
cat ./test/K8sOperator.NET.Tests/K8sOperator.NET.Tests.csproj

Repository: pmdevers/K8sOperator.NET

Length of output: 684


🏁 Script executed:

# Check for .editorconfig files
find . -name ".editorconfig" -type f

Repository: pmdevers/K8sOperator.NET

Length of output: 83


🏁 Script executed:

# Check how many other async Task tests exist in this file without await
rg -n "public async Task" ./test/K8sOperator.NET.Tests/OperatorConfiguration_Tests.cs -A 20 | grep -E "public async Task|await|^\s*\}" | head -50

Repository: pmdevers/K8sOperator.NET

Length of output: 3823


🏁 Script executed:

# Check .editorconfig for CS1998 or warning settings
cat ./.editorconfig

Repository: pmdevers/K8sOperator.NET

Length of output: 12941


🏁 Script executed:

# Check the line 300 test to see if it also has await
sed -n '300,320p' ./test/K8sOperator.NET.Tests/OperatorConfiguration_Tests.cs

Repository: pmdevers/K8sOperator.NET

Length of output: 690


Remove unnecessary async from test method.

This test has async Task but no await, which produces CS1998 warnings. Since the test only calls synchronous methods, remove async and the method signature to Task or change to void. The suggested fix below removes the async keyword entirely, which is the cleanest approach.

✅ Suggested fix
-    public async Task Validate_Should_NotThrow_WhenAllPropertiesAreValid()
+    public void Validate_Should_NotThrow_WhenAllPropertiesAreValid()
     {
         // Arrange
         var config = new OperatorConfiguration
         {
             OperatorName = "my-operator",
             Namespace = "default",
             ContainerRegistry = "ghcr.io",
             ContainerRepository = "myorg/myapp",
             ContainerTag = "1.0.0"
         };

         // Act & Assert - Should not throw
         config.Validate();
     }
🤖 Prompt for AI Agents
In `@test/K8sOperator.NET.Tests/OperatorConfiguration_Tests.cs` around lines 283 -
297, The test method Validate_Should_NotThrow_WhenAllPropertiesAreValid is
declared async but contains no awaits; remove the unnecessary async and change
the signature to a synchronous test (e.g., "public void
Validate_Should_NotThrow_WhenAllPropertiesAreValid()") in the
OperatorConfiguration_Tests class so you no longer get CS1998 warnings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/K8sOperator.NET/K8sOperator.NET.csproj`:
- Around line 53-64: The PackBuildTasks target currently only includes
K8sOperator.NET.BuildTasks.dll, so change the _BuildTasksFiles item to include
all outputs from the BuildTasks bin folder (e.g., use a wildcard like
Include="..\K8sOperator.NET.BuildTasks\bin\$(Configuration)\netstandard2.0\**\*.*"
or otherwise enumerate all *.dll and related files) and then pack that item list
to both PackagePath="build\netstandard2.0" and "buildTransitive\netstandard2.0"
(keep the existing None Include lines but reference @(_BuildTasksFiles) so
Microsoft.Build.Framework.dll, Microsoft.Build.Utilities.Core.dll and any other
outputs are included alongside K8sOperator.NET.BuildTasks.dll). Ensure the
changes are made inside the Target named PackBuildTasks and update the
_BuildTasksFiles item and subsequent None entries accordingly.
♻️ Duplicate comments (4)
src/K8sOperator.NET.BuildTasks/Templates/launchSettings.json.template (1)

11-14: Remove shell redirection from the Install profile.

dotnet run won’t interpret > in launchSettings, so install.yaml won’t be created at Line 13. Use a CLI output flag (if supported) or keep redirection as a manual shell step.

🔧 Suggested fix
-      "commandLineArgs": "install > install.yaml",
+      "commandLineArgs": "install",
examples/SimpleOperator/Properties/launchSettings.json (1)

11-14: Remove shell redirection from the Install profile.

dotnet run doesn’t interpret > in launchSettings, so install.yaml won’t be created at Line 13. Prefer an explicit output flag or keep redirection as a manual shell step.

🔧 Suggested fix
-      "commandLineArgs": "install > install.yaml",
+      "commandLineArgs": "install",
src/K8sOperator.NET/Configuration/OperatorConfigurationProvider.cs (1)

71-80: Avoid Bind() overwriting attribute defaults with empty strings.

If the "Operator" section contains empty values, Bind() will overwrite non-empty assembly-derived defaults at Line 80. Prefer selective assignment for non-empty values.

🔧 Suggested fix (selective binding)
     private void ApplyConfiguration(OperatorConfiguration config)
     {
         if (configuration == null)
             return;

         // Bind from appsettings.json section: "Operator"
         var section = configuration.GetSection("Operator");
         if (section.Exists())
         {
-            section.Bind(config);
+            var operatorName = section["OperatorName"];
+            if (!string.IsNullOrEmpty(operatorName))
+                config.OperatorName = operatorName;
+
+            var ns = section["Namespace"];
+            if (!string.IsNullOrEmpty(ns))
+                config.Namespace = ns;
+
+            var registry = section["ContainerRegistry"];
+            if (!string.IsNullOrEmpty(registry))
+                config.ContainerRegistry = registry;
+
+            var repository = section["ContainerRepository"];
+            if (!string.IsNullOrEmpty(repository))
+                config.ContainerRepository = repository;
+
+            var tag = section["ContainerTag"];
+            if (!string.IsNullOrEmpty(tag))
+                config.ContainerTag = tag;
         }
     }
In Microsoft.Extensions.Configuration.Binder, does section.Bind(...) overwrite existing property values with empty strings when the configuration value is an empty string?
src/K8sOperator.NET/Commands/VersionCommand.cs (1)

16-25: Guard ContainerImage access to avoid runtime failure.

Line 18’s ContainerTag check is redundant because it defaults to "latest", while Line 25 can throw InvalidOperationException if ContainerRegistry/ContainerRepository aren’t configured. Consider validating or handling the exception so the command doesn’t crash.

🛠️ Suggested fix
-        if (string.IsNullOrWhiteSpace(config.OperatorName) || string.IsNullOrWhiteSpace(config.ContainerTag))
+        if (string.IsNullOrWhiteSpace(config.OperatorName))
         {
             Console.WriteLine("Operator name or version metadata is missing.");
             return Task.CompletedTask;
         }

         Console.WriteLine($"{config.OperatorName} version {config.ContainerTag}.");
-        Console.WriteLine($"Docker Info: {config.ContainerImage}.");
+        try
+        {
+            Console.WriteLine($"Docker Info: {config.ContainerImage}.");
+        }
+        catch (InvalidOperationException)
+        {
+            Console.WriteLine("Docker Info: Not fully configured.");
+        }
🧹 Nitpick comments (4)
test/K8sOperator.NET.Tests/EventWatcherDatasource_Tests.cs (1)

33-47: Consider renaming metadata locals to configuration for clarity.

Those locals now hold OperatorConfiguration, so the current name is misleading and easy to misread when scanning tests.

✏️ Example rename (apply consistently)
-        var metadata = CreateConfiguration();
+        var configuration = CreateConfiguration();

-        var datasource = new EventWatcherDatasource(serviceProvider, metadata);
+        var datasource = new EventWatcherDatasource(serviceProvider, configuration);

-        await Assert.That(datasource.Configuration).IsEqualTo(metadata);
+        await Assert.That(datasource.Configuration).IsEqualTo(configuration);
src/K8sOperator.NET/Operator.targets (1)

3-11: Avoid hard‑coding Debug/Release in task assembly path.

Custom configurations (e.g., CI, ReleaseSigned) won’t resolve the task DLL. Consider using $(Configuration) for a single, resilient lookup.

♻️ Proposed refactor
-		<_BuildTasksPath Condition="'$(_BuildTasksPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Debug\netstandard2.0\K8sOperator.NET.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Debug\netstandard2.0\K8sOperator.NET.BuildTasks.dll</_BuildTasksPath>
-		<_BuildTasksPath Condition="'$(_BuildTasksPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Release\netstandard2.0\K8sOperator.NET.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\Release\netstandard2.0\K8sOperator.NET.BuildTasks.dll</_BuildTasksPath>
+		<_BuildTasksPath Condition="'$(_BuildTasksPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\$(Configuration)\netstandard2.0\K8sOperator.NET.BuildTasks.dll')">$(MSBuildThisFileDirectory)..\K8sOperator.NET.BuildTasks\bin\$(Configuration)\netstandard2.0\K8sOperator.NET.BuildTasks.dll</_BuildTasksPath>
src/K8sOperator.NET/OperatorExtensions.cs (1)

24-47: Consider validating OperatorConfiguration after overrides.

Fail-fast here prevents late runtime surprises if required fields are missing or malformed.

♻️ Proposed refinement
                 if (builder.OperatorConfiguration != null)
                 {
                     if (!string.IsNullOrEmpty(builder.OperatorConfiguration.OperatorName))
                         config.OperatorName = builder.OperatorConfiguration.OperatorName;
                     if (!string.IsNullOrEmpty(builder.OperatorConfiguration.Namespace))
                         config.Namespace = builder.OperatorConfiguration.Namespace;
                     if (!string.IsNullOrEmpty(builder.OperatorConfiguration.ContainerRegistry))
                         config.ContainerRegistry = builder.OperatorConfiguration.ContainerRegistry;
                     if (!string.IsNullOrEmpty(builder.OperatorConfiguration.ContainerRepository))
                         config.ContainerRepository = builder.OperatorConfiguration.ContainerRepository;
                     if (!string.IsNullOrEmpty(builder.OperatorConfiguration.ContainerTag))
                         config.ContainerTag = builder.OperatorConfiguration.ContainerTag;
                 }
 
+                config.Validate();
                 return config;
examples/SimpleOperator/Program.cs (1)

7-10: Leader election enabled—ensure Lease RBAC is granted.

The example will now require coordination.k8s.io/leases create/update/get in the operator namespace; otherwise startup can fail.

Moves code generation logic to MSBuild tasks, which improves the build process and allows for more flexibility in customizing the generated output.

Removes the commands for generating launch settings and Dockerfile, replacing them with MSBuild tasks that automatically generate these files during the build process.

Adds a configuration provider to centralize and prioritize operator configuration from assembly attributes, appsettings.json, and the OperatorBuilder, enabling easier customization.

Updates EventWatcher and commands to use the new OperatorConfiguration for settings like operator name, image, and namespace.

Adds new build tasks project and templates.

Fixes #2
@pmdevers pmdevers merged commit 9a63c3a into main Jan 23, 2026
6 checks passed
@pmdevers pmdevers deleted the build-tasks branch January 23, 2026 23:14
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant