Skip to content

Commit

Permalink
refactor: CLI Argument parsing (#615)
Browse files Browse the repository at this point in the history
This adds "System.CommandLine" as default parser
and spectre.console for beautiful output.

BREAKING CHANGE: generator commands
may define a csproj or sln file on where the
entities or other elements are located.
If no file is provided, the current directory
is searched and the command fails if none is found.
  • Loading branch information
buehler authored Sep 27, 2023
1 parent ba5dda3 commit 34b3dcf
Show file tree
Hide file tree
Showing 14 changed files with 458 additions and 298 deletions.
37 changes: 37 additions & 0 deletions src/KubeOps.Cli/Arguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.CommandLine;

namespace KubeOps.Cli;

internal static class Arguments
{
public static readonly Argument<FileInfo> SolutionOrProjectFile = new(
"sln/csproj file",
() =>
{
var projectFile
= Directory.EnumerateFiles(
Directory.GetCurrentDirectory(),
"*.csproj")
.Select(f => new FileInfo(f))
.FirstOrDefault();
var slnFile
= Directory.EnumerateFiles(
Directory.GetCurrentDirectory(),
"*.sln")
.Select(f => new FileInfo(f))
.FirstOrDefault();

return (projectFile, slnFile) switch
{
({ } prj, _) => prj,
(_, { } sln) => sln,
_ => throw new FileNotFoundException(
"No *.csproj or *.sln file found in current directory.",
Directory.GetCurrentDirectory()),
};
},
"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. " +
"This behaviour can be filtered by using the --project and --target-framework option.");
}
15 changes: 0 additions & 15 deletions src/KubeOps.Cli/Commands/Entrypoint.cs

This file was deleted.

103 changes: 50 additions & 53 deletions src/KubeOps.Cli/Commands/Generator/CrdGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,83 +1,80 @@
using KubeOps.Abstractions.Kustomize;
using System.CommandLine;
using System.CommandLine.Help;
using System.CommandLine.Invocation;

using KubeOps.Abstractions.Kustomize;
using KubeOps.Cli.Output;
using KubeOps.Cli.SyntaxObjects;
using KubeOps.Cli.Roslyn;

using McMaster.Extensions.CommandLineUtils;
using Spectre.Console;

namespace KubeOps.Cli.Commands.Generator;

[Command("crd", "crds", Description = "Generates the needed CRD for kubernetes. (Aliases: crds)")]
internal class CrdGenerator
internal static class CrdGenerator
{
private readonly ConsoleOutput _output;
private readonly ResultOutput _result;

public CrdGenerator(ConsoleOutput output, ResultOutput result)
public static Command Command
{
_output = output;
_result = result;
}

[Option(
Description = "The path the command will write the files to. If empty, prints output to console.",
LongName = "out")]
public string? OutputPath { get; set; }

[Option(
CommandOptionType.SingleValue,
Description = "Sets the output format for the generator.")]
public OutputFormat Format { get; set; }
get
{
var cmd = new Command("crd", "Generates CRDs for Kubernetes based on a solution or project.")
{
Options.OutputFormat,
Options.OutputPath,
Options.SolutionProjectRegex,
Options.TargetFramework,
Arguments.SolutionOrProjectFile,
};
cmd.AddAlias("crds");
cmd.AddAlias("c");
cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx));

[Argument(
0,
Description =
"Path to a *.csproj file to generate the CRD from. " +
"If omitted, the current directory is searched for one and the command fails if none is found.")]
public string? ProjectFile { get; set; }
return cmd;
}
}

public async Task<int> OnExecuteAsync()
internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
{
_result.Format = Format;
var projectFile = ProjectFile ??
Directory.EnumerateFiles(
Directory.GetCurrentDirectory(),
"*.csproj")
.FirstOrDefault();
if (projectFile == null)
{
_output.WriteLine(
"No *.csproj file found. Either specify one or run the command in a directory with one.",
ConsoleColor.Red);
return ExitCodes.Error;
}
var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);

_output.WriteLine($"Generate CRDs from project: {projectFile}.");
var parser = file.Extension switch
{
".csproj" => await AssemblyParser.ForProject(console, file),
".sln" => await AssemblyParser.ForSolution(
console,
file,
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
};
var result = new ResultOutput(console, format);

var parser = new ProjectParser(projectFile);
var crds = Transpiler.Crds.Transpile(await parser.Entities().ToListAsync()).ToList();
console.WriteLine($"Generate CRDs for {file.Name}.");
var crds = Transpiler.Crds.Transpile(parser.Entities()).ToList();
foreach (var crd in crds)
{
_result.Add($"{crd.Metadata.Name.Replace('.', '_')}.{Format.ToString().ToLowerInvariant()}", crd);
result.Add($"{crd.Metadata.Name.Replace('.', '_')}.{format.ToString().ToLowerInvariant()}", crd);
}

_result.Add(
$"kustomization.{Format.ToString().ToLowerInvariant()}",
result.Add(
$"kustomization.{format.ToString().ToLowerInvariant()}",
new KustomizationConfig
{
Resources = crds
.ConvertAll(crd => $"{crd.Metadata.Name.Replace('.', '_')}.{Format.ToString().ToLower()}"),
.ConvertAll(crd => $"{crd.Metadata.Name.Replace('.', '_')}.{format.ToString().ToLower()}"),
CommonLabels = new Dictionary<string, string> { { "operator-element", "crd" } },
});

if (OutputPath is not null)
if (outPath is not null)
{
await _result.Write(OutputPath);
await result.Write(outPath);
}
else
{
_result.Write();
result.Write();
}

return ExitCodes.Success;
ctx.ExitCode = ExitCodes.Success;
}
}
25 changes: 17 additions & 8 deletions src/KubeOps.Cli/Commands/Generator/Generator.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
using McMaster.Extensions.CommandLineUtils;
using System.CommandLine;
using System.CommandLine.Help;

namespace KubeOps.Cli.Commands.Generator;

[Command("generator", "gen", "g", Description = "Generates elements related to an operator. (Aliases: gen, g)")]
[Subcommand(typeof(CrdGenerator))]
[Subcommand(typeof(RbacGenerator))]
internal class Generator
internal static class Generator
{
public int OnExecute(CommandLineApplication app)
public static Command Command
{
app.ShowHelp();
return ExitCodes.UsageError;
get
{
var cmd = new Command("generator", "Generates elements related to an operator.")
{
CrdGenerator.Command,
RbacGenerator.Command,
};
cmd.AddAlias("gen");
cmd.AddAlias("g");
cmd.SetHandler(ctx => ctx.HelpBuilder.Write(cmd, Console.Out));

return cmd;
}
}
}
97 changes: 46 additions & 51 deletions src/KubeOps.Cli/Commands/Generator/RbacGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,70 +1,65 @@
using KubeOps.Cli.Output;
using KubeOps.Cli.SyntaxObjects;
using System.CommandLine;
using System.CommandLine.Help;
using System.CommandLine.Invocation;

using McMaster.Extensions.CommandLineUtils;
using KubeOps.Abstractions.Kustomize;
using KubeOps.Cli.Output;
using KubeOps.Cli.Roslyn;

using Spectre.Console;

namespace KubeOps.Cli.Commands.Generator;

[Command("rbac", "r", Description = "Generates rbac roles for the operator. (Aliases: r)")]
internal class RbacGenerator
internal static class RbacGenerator
{
private readonly ConsoleOutput _output;
private readonly ResultOutput _result;

public RbacGenerator(ConsoleOutput output, ResultOutput result)
{
_output = output;
_result = result;
}

[Option(
Description = "The path the command will write the files to. If empty, prints output to console.",
LongName = "out")]
public string? OutputPath { get; set; }

[Option(
CommandOptionType.SingleValue,
Description = "Sets the output format for the generator.")]
public OutputFormat Format { get; set; }

[Argument(
0,
Description =
"Path to a *.csproj file to generate the CRD from. " +
"If omitted, the current directory is searched for one and the command fails if none is found.")]
public string? ProjectFile { get; set; }

public async Task<int> OnExecuteAsync()
public static Command Command
{
_result.Format = Format;
var projectFile = ProjectFile ??
Directory.EnumerateFiles(
Directory.GetCurrentDirectory(),
"*.csproj")
.FirstOrDefault();
if (projectFile == null)
get
{
_output.WriteLine(
"No *.csproj file found. Either specify one or run the command in a directory with one.",
ConsoleColor.Red);
return ExitCodes.Error;
var cmd = new Command("rbac", "Generates rbac roles for the operator project or solution.")
{
Options.OutputFormat,
Options.OutputPath,
Options.SolutionProjectRegex,
Options.TargetFramework,
Arguments.SolutionOrProjectFile,
};
cmd.AddAlias("r");
cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx));

return cmd;
}
}

_output.WriteLine($"Generate CRDs from project: {projectFile}.");
internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
{
var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);

var parser = new ProjectParser(projectFile);
var attributes = await parser.RbacAttributes().ToListAsync();
_result.Add("file.yaml", Transpiler.Rbac.Transpile(attributes));
var parser = file.Extension switch
{
".csproj" => await AssemblyParser.ForProject(console, file),
".sln" => await AssemblyParser.ForSolution(
console,
file,
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
};
var result = new ResultOutput(console, format);
console.WriteLine($"Generate RBAC roles for {file.Name}.");
result.Add("file.yaml", Transpiler.Rbac.Transpile(parser.RbacAttributes()));

if (OutputPath is not null)
if (outPath is not null)
{
await _result.Write(OutputPath);
await result.Write(outPath);
}
else
{
_result.Write();
result.Write();
}

return ExitCodes.Success;
ctx.ExitCode = ExitCodes.Success;
}
}
44 changes: 26 additions & 18 deletions src/KubeOps.Cli/Commands/Utilities/Version.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
using k8s;
using System.CommandLine;

using McMaster.Extensions.CommandLineUtils;
using k8s;

using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;

namespace KubeOps.Cli.Commands.Utilities;

[Command(
"api-version",
"av",
Description = "Prints the actual server version of the connected kubernetes cluster. (Aliases: av)")]
internal class Version
internal static class Version
{
public async Task<int> OnExecuteAsync(CommandLineApplication app)
public static Command Command
{
get
{
var cmd = new Command("api-version", "Prints the actual server version of the connected kubernetes cluster.");
cmd.AddAlias("av");
cmd.SetHandler(() =>
Handler(AnsiConsole.Console, new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig())));

return cmd;
}
}

internal static async Task<int> Handler(IAnsiConsole console, IKubernetes client)
{
var client = app.GetRequiredService<IKubernetes>();
var version = await client.Version.GetCodeAsync();
await app.Out.WriteLineAsync(
$"""
The Kubernetes API reported the following version:
Git-Version: {version.GitVersion}
Major: {version.Major}
Minor: {version.Minor}
Platform: {version.Platform}
""");
console.Write(new Table()
.Title("Kubernetes API Version")
.HideHeaders()
.AddColumns("Info", "Value")
.AddRow("Git-Version", version.GitVersion)
.AddRow("Major", version.Major)
.AddRow("Minor", version.Minor)
.AddRow("Platform", version.Platform));

return ExitCodes.Success;
}
Expand Down
Loading

0 comments on commit 34b3dcf

Please sign in to comment.