Skip to content

Commit 34b3dcf

Browse files
authored
refactor: CLI Argument parsing (#615)
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.
1 parent ba5dda3 commit 34b3dcf

File tree

14 files changed

+458
-298
lines changed

14 files changed

+458
-298
lines changed

src/KubeOps.Cli/Arguments.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.CommandLine;
2+
3+
namespace KubeOps.Cli;
4+
5+
internal static class Arguments
6+
{
7+
public static readonly Argument<FileInfo> SolutionOrProjectFile = new(
8+
"sln/csproj file",
9+
() =>
10+
{
11+
var projectFile
12+
= Directory.EnumerateFiles(
13+
Directory.GetCurrentDirectory(),
14+
"*.csproj")
15+
.Select(f => new FileInfo(f))
16+
.FirstOrDefault();
17+
var slnFile
18+
= Directory.EnumerateFiles(
19+
Directory.GetCurrentDirectory(),
20+
"*.sln")
21+
.Select(f => new FileInfo(f))
22+
.FirstOrDefault();
23+
24+
return (projectFile, slnFile) switch
25+
{
26+
({ } prj, _) => prj,
27+
(_, { } sln) => sln,
28+
_ => throw new FileNotFoundException(
29+
"No *.csproj or *.sln file found in current directory.",
30+
Directory.GetCurrentDirectory()),
31+
};
32+
},
33+
"A solution or project file where entities are located. " +
34+
"If omitted, the current directory is searched for a *.csproj or *.sln file. " +
35+
"If an *.sln file is used, all projects in the solution (with the newest framework) will be searched for entities. " +
36+
"This behaviour can be filtered by using the --project and --target-framework option.");
37+
}

src/KubeOps.Cli/Commands/Entrypoint.cs

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 50 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,80 @@
1-
using KubeOps.Abstractions.Kustomize;
1+
using System.CommandLine;
2+
using System.CommandLine.Help;
3+
using System.CommandLine.Invocation;
4+
5+
using KubeOps.Abstractions.Kustomize;
26
using KubeOps.Cli.Output;
3-
using KubeOps.Cli.SyntaxObjects;
7+
using KubeOps.Cli.Roslyn;
48

5-
using McMaster.Extensions.CommandLineUtils;
9+
using Spectre.Console;
610

711
namespace KubeOps.Cli.Commands.Generator;
812

9-
[Command("crd", "crds", Description = "Generates the needed CRD for kubernetes. (Aliases: crds)")]
10-
internal class CrdGenerator
13+
internal static class CrdGenerator
1114
{
12-
private readonly ConsoleOutput _output;
13-
private readonly ResultOutput _result;
14-
15-
public CrdGenerator(ConsoleOutput output, ResultOutput result)
15+
public static Command Command
1616
{
17-
_output = output;
18-
_result = result;
19-
}
20-
21-
[Option(
22-
Description = "The path the command will write the files to. If empty, prints output to console.",
23-
LongName = "out")]
24-
public string? OutputPath { get; set; }
25-
26-
[Option(
27-
CommandOptionType.SingleValue,
28-
Description = "Sets the output format for the generator.")]
29-
public OutputFormat Format { get; set; }
17+
get
18+
{
19+
var cmd = new Command("crd", "Generates CRDs for Kubernetes based on a solution or project.")
20+
{
21+
Options.OutputFormat,
22+
Options.OutputPath,
23+
Options.SolutionProjectRegex,
24+
Options.TargetFramework,
25+
Arguments.SolutionOrProjectFile,
26+
};
27+
cmd.AddAlias("crds");
28+
cmd.AddAlias("c");
29+
cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx));
3030

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

38-
public async Task<int> OnExecuteAsync()
35+
internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
3936
{
40-
_result.Format = Format;
41-
var projectFile = ProjectFile ??
42-
Directory.EnumerateFiles(
43-
Directory.GetCurrentDirectory(),
44-
"*.csproj")
45-
.FirstOrDefault();
46-
if (projectFile == null)
47-
{
48-
_output.WriteLine(
49-
"No *.csproj file found. Either specify one or run the command in a directory with one.",
50-
ConsoleColor.Red);
51-
return ExitCodes.Error;
52-
}
37+
var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
38+
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
39+
var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);
5340

54-
_output.WriteLine($"Generate CRDs from project: {projectFile}.");
41+
var parser = file.Extension switch
42+
{
43+
".csproj" => await AssemblyParser.ForProject(console, file),
44+
".sln" => await AssemblyParser.ForSolution(
45+
console,
46+
file,
47+
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
48+
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
49+
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
50+
};
51+
var result = new ResultOutput(console, format);
5552

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

63-
_result.Add(
64-
$"kustomization.{Format.ToString().ToLowerInvariant()}",
60+
result.Add(
61+
$"kustomization.{format.ToString().ToLowerInvariant()}",
6562
new KustomizationConfig
6663
{
6764
Resources = crds
68-
.ConvertAll(crd => $"{crd.Metadata.Name.Replace('.', '_')}.{Format.ToString().ToLower()}"),
65+
.ConvertAll(crd => $"{crd.Metadata.Name.Replace('.', '_')}.{format.ToString().ToLower()}"),
6966
CommonLabels = new Dictionary<string, string> { { "operator-element", "crd" } },
7067
});
7168

72-
if (OutputPath is not null)
69+
if (outPath is not null)
7370
{
74-
await _result.Write(OutputPath);
71+
await result.Write(outPath);
7572
}
7673
else
7774
{
78-
_result.Write();
75+
result.Write();
7976
}
8077

81-
return ExitCodes.Success;
78+
ctx.ExitCode = ExitCodes.Success;
8279
}
8380
}
Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
using McMaster.Extensions.CommandLineUtils;
1+
using System.CommandLine;
2+
using System.CommandLine.Help;
23

34
namespace KubeOps.Cli.Commands.Generator;
45

5-
[Command("generator", "gen", "g", Description = "Generates elements related to an operator. (Aliases: gen, g)")]
6-
[Subcommand(typeof(CrdGenerator))]
7-
[Subcommand(typeof(RbacGenerator))]
8-
internal class Generator
6+
internal static class Generator
97
{
10-
public int OnExecute(CommandLineApplication app)
8+
public static Command Command
119
{
12-
app.ShowHelp();
13-
return ExitCodes.UsageError;
10+
get
11+
{
12+
var cmd = new Command("generator", "Generates elements related to an operator.")
13+
{
14+
CrdGenerator.Command,
15+
RbacGenerator.Command,
16+
};
17+
cmd.AddAlias("gen");
18+
cmd.AddAlias("g");
19+
cmd.SetHandler(ctx => ctx.HelpBuilder.Write(cmd, Console.Out));
20+
21+
return cmd;
22+
}
1423
}
1524
}
Lines changed: 46 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,65 @@
1-
using KubeOps.Cli.Output;
2-
using KubeOps.Cli.SyntaxObjects;
1+
using System.CommandLine;
2+
using System.CommandLine.Help;
3+
using System.CommandLine.Invocation;
34

4-
using McMaster.Extensions.CommandLineUtils;
5+
using KubeOps.Abstractions.Kustomize;
6+
using KubeOps.Cli.Output;
7+
using KubeOps.Cli.Roslyn;
8+
9+
using Spectre.Console;
510

611
namespace KubeOps.Cli.Commands.Generator;
712

8-
[Command("rbac", "r", Description = "Generates rbac roles for the operator. (Aliases: r)")]
9-
internal class RbacGenerator
13+
internal static class RbacGenerator
1014
{
11-
private readonly ConsoleOutput _output;
12-
private readonly ResultOutput _result;
13-
14-
public RbacGenerator(ConsoleOutput output, ResultOutput result)
15-
{
16-
_output = output;
17-
_result = result;
18-
}
19-
20-
[Option(
21-
Description = "The path the command will write the files to. If empty, prints output to console.",
22-
LongName = "out")]
23-
public string? OutputPath { get; set; }
24-
25-
[Option(
26-
CommandOptionType.SingleValue,
27-
Description = "Sets the output format for the generator.")]
28-
public OutputFormat Format { get; set; }
29-
30-
[Argument(
31-
0,
32-
Description =
33-
"Path to a *.csproj file to generate the CRD from. " +
34-
"If omitted, the current directory is searched for one and the command fails if none is found.")]
35-
public string? ProjectFile { get; set; }
36-
37-
public async Task<int> OnExecuteAsync()
15+
public static Command Command
3816
{
39-
_result.Format = Format;
40-
var projectFile = ProjectFile ??
41-
Directory.EnumerateFiles(
42-
Directory.GetCurrentDirectory(),
43-
"*.csproj")
44-
.FirstOrDefault();
45-
if (projectFile == null)
17+
get
4618
{
47-
_output.WriteLine(
48-
"No *.csproj file found. Either specify one or run the command in a directory with one.",
49-
ConsoleColor.Red);
50-
return ExitCodes.Error;
19+
var cmd = new Command("rbac", "Generates rbac roles for the operator project or solution.")
20+
{
21+
Options.OutputFormat,
22+
Options.OutputPath,
23+
Options.SolutionProjectRegex,
24+
Options.TargetFramework,
25+
Arguments.SolutionOrProjectFile,
26+
};
27+
cmd.AddAlias("r");
28+
cmd.SetHandler(ctx => Handler(AnsiConsole.Console, ctx));
29+
30+
return cmd;
5131
}
32+
}
5233

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

55-
var parser = new ProjectParser(projectFile);
56-
var attributes = await parser.RbacAttributes().ToListAsync();
57-
_result.Add("file.yaml", Transpiler.Rbac.Transpile(attributes));
40+
var parser = file.Extension switch
41+
{
42+
".csproj" => await AssemblyParser.ForProject(console, file),
43+
".sln" => await AssemblyParser.ForSolution(
44+
console,
45+
file,
46+
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
47+
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
48+
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
49+
};
50+
var result = new ResultOutput(console, format);
51+
console.WriteLine($"Generate RBAC roles for {file.Name}.");
52+
result.Add("file.yaml", Transpiler.Rbac.Transpile(parser.RbacAttributes()));
5853

59-
if (OutputPath is not null)
54+
if (outPath is not null)
6055
{
61-
await _result.Write(OutputPath);
56+
await result.Write(outPath);
6257
}
6358
else
6459
{
65-
_result.Write();
60+
result.Write();
6661
}
6762

68-
return ExitCodes.Success;
63+
ctx.ExitCode = ExitCodes.Success;
6964
}
7065
}

src/KubeOps.Cli/Commands/Utilities/Version.cs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
1-
using k8s;
1+
using System.CommandLine;
22

3-
using McMaster.Extensions.CommandLineUtils;
3+
using k8s;
44

5-
using Microsoft.Extensions.DependencyInjection;
5+
using Spectre.Console;
66

77
namespace KubeOps.Cli.Commands.Utilities;
88

9-
[Command(
10-
"api-version",
11-
"av",
12-
Description = "Prints the actual server version of the connected kubernetes cluster. (Aliases: av)")]
13-
internal class Version
9+
internal static class Version
1410
{
15-
public async Task<int> OnExecuteAsync(CommandLineApplication app)
11+
public static Command Command
12+
{
13+
get
14+
{
15+
var cmd = new Command("api-version", "Prints the actual server version of the connected kubernetes cluster.");
16+
cmd.AddAlias("av");
17+
cmd.SetHandler(() =>
18+
Handler(AnsiConsole.Console, new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig())));
19+
20+
return cmd;
21+
}
22+
}
23+
24+
internal static async Task<int> Handler(IAnsiConsole console, IKubernetes client)
1625
{
17-
var client = app.GetRequiredService<IKubernetes>();
1826
var version = await client.Version.GetCodeAsync();
19-
await app.Out.WriteLineAsync(
20-
$"""
21-
The Kubernetes API reported the following version:
22-
Git-Version: {version.GitVersion}
23-
Major: {version.Major}
24-
Minor: {version.Minor}
25-
Platform: {version.Platform}
26-
""");
27+
console.Write(new Table()
28+
.Title("Kubernetes API Version")
29+
.HideHeaders()
30+
.AddColumns("Info", "Value")
31+
.AddRow("Git-Version", version.GitVersion)
32+
.AddRow("Major", version.Major)
33+
.AddRow("Minor", version.Minor)
34+
.AddRow("Platform", version.Platform));
2735

2836
return ExitCodes.Success;
2937
}

0 commit comments

Comments
 (0)