From 2d0285864c092b415db65313106fb10a97bf727e Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Tue, 5 May 2026 17:48:49 +0200 Subject: [PATCH 1/4] feat(cli): support new slnx solution format --- docs/docs/operator/cli.mdx | 4 ++++ src/KubeOps.Cli/Arguments.cs | 17 ++++++++++++----- .../Commands/Generator/OperatorGenerator.cs | 4 ++-- src/KubeOps.Cli/Commands/Management/Install.cs | 4 ++-- .../Commands/Management/Uninstall.cs | 4 ++-- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/docs/operator/cli.mdx b/docs/docs/operator/cli.mdx index a0c44d36..81350e79 100644 --- a/docs/docs/operator/cli.mdx +++ b/docs/docs/operator/cli.mdx @@ -45,6 +45,10 @@ This command: - Generates the necessary CRDs - Installs them into your current Kubernetes context +:::tip[Solution files] +Instead of a `.csproj` file, you can pass a `.sln` or `.slnx` solution file. The CLI will compile all projects in the solution and search them for custom resources. Use `--project` (regex) and `--target-framework` to narrow the scope. +::: + :::warning[Production Usage] The `install` and `uninstall` commands are primarily intended for development purposes. In production, you should use the generated Kubernetes manifests and your preferred deployment method (e.g., Helm, Kustomize, or GitOps). ::: diff --git a/src/KubeOps.Cli/Arguments.cs b/src/KubeOps.Cli/Arguments.cs index cc70db2b..7f76e9e6 100644 --- a/src/KubeOps.Cli/Arguments.cs +++ b/src/KubeOps.Cli/Arguments.cs @@ -24,10 +24,17 @@ var slnFile "*.sln") .Select(f => new FileInfo(f)) .FirstOrDefault(); - var file = (projectFile, slnFile) switch + var slnxFile + = Directory.EnumerateFiles( + Directory.GetCurrentDirectory(), + "*.slnx") + .Select(f => new FileInfo(f)) + .FirstOrDefault(); + var file = (projectFile, slnFile, slnxFile) switch { - ({ } prj, _) => prj, - (_, { } sln) => sln, + ({ } prj, _, _) => prj, + (_, { } sln, _) => sln, + (_, _, { } slnx) => slnx, _ => null, }; @@ -40,8 +47,8 @@ var slnFile return new FileInfo("not-found"); }, Description = "A solution or project file where entities are located. " + - "If omitted, the current directory is searched for a *.csproj or *.sln file. " + - "If an *.sln file is used, all projects in the solution (with the newest framework) will be searched for entities. " + + "If omitted, the current directory is searched for a *.csproj, *.sln, or *.slnx file. " + + "If an *.sln or *.slnx file is used, all projects in the solution (with the newest framework) will be searched for entities. " + "This behaviour can be filtered by using the --project and --target-framework option.", }; diff --git a/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs b/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs index 2229e37c..5548d117 100644 --- a/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs +++ b/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs @@ -68,13 +68,13 @@ internal static async Task Handler(IAnsiConsole console, ParseResult parseR var parser = file switch { { Extension: ".csproj", Exists: true } => await AssemblyLoader.ForProject(console, file), - { Extension: ".sln", Exists: true } => await AssemblyLoader.ForSolution( + { Extension: ".sln" or ".slnx", Exists: true } => await AssemblyLoader.ForSolution( console, file, parseResult.GetValue(Options.SolutionProjectRegex), parseResult.GetValue(Options.TargetFramework)), { Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."), - _ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."), + _ => throw new NotSupportedException("Only *.csproj, *.sln, and *.slnx files are supported."), }; var mutators = parser.GetMutatedEntities().ToList(); diff --git a/src/KubeOps.Cli/Commands/Management/Install.cs b/src/KubeOps.Cli/Commands/Management/Install.cs index b0e67dcb..9cde0eaf 100644 --- a/src/KubeOps.Cli/Commands/Management/Install.cs +++ b/src/KubeOps.Cli/Commands/Management/Install.cs @@ -52,13 +52,13 @@ internal static async Task Handler(IAnsiConsole console, IKubernetes client var parser = file switch { { Extension: ".csproj", Exists: true } => await AssemblyLoader.ForProject(console, file), - { Extension: ".sln", Exists: true } => await AssemblyLoader.ForSolution( + { Extension: ".sln" or ".slnx", Exists: true } => await AssemblyLoader.ForSolution( console, file, parseResult.GetValue(Options.SolutionProjectRegex), parseResult.GetValue(Options.TargetFramework)), { Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."), - _ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."), + _ => throw new NotSupportedException("Only *.csproj, *.sln, and *.slnx files are supported."), }; console.WriteLine($"Install CRDs from {file.Name}."); diff --git a/src/KubeOps.Cli/Commands/Management/Uninstall.cs b/src/KubeOps.Cli/Commands/Management/Uninstall.cs index e938e8f5..c839a5d1 100644 --- a/src/KubeOps.Cli/Commands/Management/Uninstall.cs +++ b/src/KubeOps.Cli/Commands/Management/Uninstall.cs @@ -52,13 +52,13 @@ internal static async Task Handler(IAnsiConsole console, IKubernetes client var parser = file switch { { Extension: ".csproj", Exists: true } => await AssemblyLoader.ForProject(console, file), - { Extension: ".sln", Exists: true } => await AssemblyLoader.ForSolution( + { Extension: ".sln" or ".slnx", Exists: true } => await AssemblyLoader.ForSolution( console, file, parseResult.GetValue(Options.SolutionProjectRegex), parseResult.GetValue(Options.TargetFramework)), { Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."), - _ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."), + _ => throw new NotSupportedException("Only *.csproj, *.sln, and *.slnx files are supported."), }; console.WriteLine($"Uninstall CRDs from {file.Name}."); From b9ae01cc3a2e19ce16e35aaedd4157e7739901b3 Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Tue, 5 May 2026 17:51:00 +0200 Subject: [PATCH 2/4] chore(docs): adjusted sln/slnx tip slightly --- docs/docs/operator/cli.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/operator/cli.mdx b/docs/docs/operator/cli.mdx index 81350e79..dbec1c8b 100644 --- a/docs/docs/operator/cli.mdx +++ b/docs/docs/operator/cli.mdx @@ -46,7 +46,7 @@ This command: - Installs them into your current Kubernetes context :::tip[Solution files] -Instead of a `.csproj` file, you can pass a `.sln` or `.slnx` solution file. The CLI will compile all projects in the solution and search them for custom resources. Use `--project` (regex) and `--target-framework` to narrow the scope. +Instead of a `.csproj` file, you can pass a `.sln` or `.slnx` solution file. The CLI will compile all projects in the solution and search them for custom resources. ::: :::warning[Production Usage] From 6e522161d1eba4650716c86bf7e17a586d1c10b8 Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Wed, 6 May 2026 08:37:42 +0200 Subject: [PATCH 3/4] chore(console): fixed some markup issues while using ansi console --- src/KubeOps.Cli/Arguments.cs | 2 +- .../Commands/Generator/OperatorGenerator.cs | 4 +-- .../Commands/Management/Install.cs | 4 +-- .../Commands/Management/Uninstall.cs | 4 +-- .../Transpilation/AssemblyLoader.cs | 26 ++++++++++++------- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/KubeOps.Cli/Arguments.cs b/src/KubeOps.Cli/Arguments.cs index 7f76e9e6..359f2191 100644 --- a/src/KubeOps.Cli/Arguments.cs +++ b/src/KubeOps.Cli/Arguments.cs @@ -8,7 +8,7 @@ namespace KubeOps.Cli; internal static class Arguments { - public static readonly Argument SolutionOrProjectFile = new("sln/csproj file") + public static readonly Argument SolutionOrProjectFile = new("sln/slnx/csproj file") { DefaultValueFactory = result => { diff --git a/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs b/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs index 5548d117..e4991905 100644 --- a/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs +++ b/src/KubeOps.Cli/Commands/Generator/OperatorGenerator.cs @@ -175,11 +175,11 @@ internal static async Task Handler(IAnsiConsole console, ParseResult parseR } catch (Exception e) { - console.MarkupLine($"[red]Could not clear output path: {e.Message}[/]"); + console.MarkupLineInterpolated($"[red]Could not clear output path: {e.Message}[/]"); } } - console.MarkupLine($"[green]Write output to {outPath}.[/]"); + console.MarkupLineInterpolated($"[green]Write output to {outPath}.[/]"); await result.Write(outPath); } else diff --git a/src/KubeOps.Cli/Commands/Management/Install.cs b/src/KubeOps.Cli/Commands/Management/Install.cs index 9cde0eaf..bd6d7c53 100644 --- a/src/KubeOps.Cli/Commands/Management/Install.cs +++ b/src/KubeOps.Cli/Commands/Management/Install.cs @@ -103,13 +103,13 @@ internal static async Task Handler(IAnsiConsole console, IKubernetes client } catch (HttpOperationException) { - console.WriteLine( + console.MarkupLineInterpolated( $"""[red]There was a http (api) error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); throw; } catch (Exception) { - console.WriteLine( + console.MarkupLineInterpolated( $"""[red]There was an error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); throw; } diff --git a/src/KubeOps.Cli/Commands/Management/Uninstall.cs b/src/KubeOps.Cli/Commands/Management/Uninstall.cs index c839a5d1..0615dbcd 100644 --- a/src/KubeOps.Cli/Commands/Management/Uninstall.cs +++ b/src/KubeOps.Cli/Commands/Management/Uninstall.cs @@ -100,13 +100,13 @@ internal static async Task Handler(IAnsiConsole console, IKubernetes client } catch (HttpOperationException) { - console.WriteLine( + console.MarkupLineInterpolated( $"""[red]There was a http (api) error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); throw; } catch (Exception) { - console.WriteLine( + console.MarkupLineInterpolated( $"""[red]There was an error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]"""); throw; } diff --git a/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs b/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs index 8812fdec..a6e32733 100644 --- a/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs +++ b/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs @@ -69,9 +69,10 @@ public static Task ForProject( console.MarkupLine("[green]Compilation successful.[/]"); console.WriteLine(); - var mlc = new MetadataLoadContext( - new PathAssemblyResolver(project.MetadataReferences.Select(m => m.Display ?? string.Empty) - .Concat(new[] { typeof(object).Assembly.Location }))); + var mlc = ContextCreator.Create( + project.MetadataReferences.Select(m => m.Display ?? string.Empty) + .Concat(new[] { typeof(object).Assembly.Location }), + coreAssemblyName: typeof(object).Assembly.GetName().Name); mlc.LoadFromByteArray(assemblyStream.ToArray()); return mlc; @@ -116,7 +117,10 @@ public static Task ForSolution( .Select(async p => { console.MarkupLineInterpolated( - $"Load compilation context for [aqua]{p.name}[/]{(p.tfm.Length > 0 ? $" [grey]{p.tfm}[/]" : string.Empty)}."); + p.tfm.Length > 0 + ? (FormattableString)$"Load compilation context for [aqua]{p.name}[/] [grey]{p.tfm}[/]." + : (FormattableString)$"Load compilation context for [aqua]{p.name}[/]."); + var compilation = await p.project.GetCompilationAsync(); console.MarkupLineInterpolated($"[green]Compilation context loaded for {p.name}.[/]"); if (compilation is null) @@ -124,9 +128,11 @@ public static Task ForSolution( throw new AggregateException("Compilation could not be found."); } - using var assemblyStream = new MemoryStream(); + await using var assemblyStream = new MemoryStream(); console.MarkupLineInterpolated( - $"Start compilation for [aqua]{p.name}[/]{(p.tfm.Length > 0 ? $" [grey]{p.tfm}[/]" : string.Empty)}."); + p.tfm.Length > 0 + ? (FormattableString)$"Start compilation for [aqua]{p.name}[/] [grey]{p.tfm}[/]." + : (FormattableString)$"Start compilation for [aqua]{p.name}[/]."); switch (compilation.Emit(assemblyStream)) { case { Success: false, Diagnostics: var diag }: @@ -140,9 +146,11 @@ public static Task ForSolution( })); console.WriteLine(); - var mlc = new MetadataLoadContext( - new PathAssemblyResolver(assemblies.SelectMany(a => a.Refs) - .Concat(new[] { typeof(object).Assembly.Location }).Distinct())); + var mlc = ContextCreator.Create( + assemblies.SelectMany(a => a.Refs) + .Concat(new[] { typeof(object).Assembly.Location }) + .Distinct(), + coreAssemblyName: typeof(object).Assembly.GetName().Name); foreach (var assembly in assemblies) { mlc.LoadFromByteArray(assembly.Assembly); From c3e8ab7f4a5e979fe842d6f2dbe7cf2f701b259d Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Wed, 6 May 2026 09:11:30 +0200 Subject: [PATCH 4/4] fix(assemblyloader): reverted MetaDataLoadContext --- src/KubeOps.Cli/Transpilation/AssemblyLoader.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs b/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs index a6e32733..51fc7a74 100644 --- a/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs +++ b/src/KubeOps.Cli/Transpilation/AssemblyLoader.cs @@ -69,10 +69,9 @@ public static Task ForProject( console.MarkupLine("[green]Compilation successful.[/]"); console.WriteLine(); - var mlc = ContextCreator.Create( - project.MetadataReferences.Select(m => m.Display ?? string.Empty) - .Concat(new[] { typeof(object).Assembly.Location }), - coreAssemblyName: typeof(object).Assembly.GetName().Name); + var mlc = new MetadataLoadContext( + new PathAssemblyResolver(project.MetadataReferences.Select(m => m.Display ?? string.Empty) + .Concat(new[] { typeof(object).Assembly.Location }))); mlc.LoadFromByteArray(assemblyStream.ToArray()); return mlc; @@ -146,11 +145,9 @@ public static Task ForSolution( })); console.WriteLine(); - var mlc = ContextCreator.Create( - assemblies.SelectMany(a => a.Refs) - .Concat(new[] { typeof(object).Assembly.Location }) - .Distinct(), - coreAssemblyName: typeof(object).Assembly.GetName().Name); + var mlc = new MetadataLoadContext( + new PathAssemblyResolver(assemblies.SelectMany(a => a.Refs) + .Concat(new[] { typeof(object).Assembly.Location }).Distinct())); foreach (var assembly in assemblies) { mlc.LoadFromByteArray(assembly.Assembly);