Skip to content

Commit 7cac2ad

Browse files
authored
feat(cli): management install/uninstall commands (#618)
Adding install / uninstall commands to the CLI. BREAKING CHANGE: The install / uninstall commands now search for a project or solution file to parse the CRDs from a solution or a project.
1 parent 34b3dcf commit 7cac2ad

File tree

14 files changed

+300
-34
lines changed

14 files changed

+300
-34
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# KubeOps
22

3-
![Code Security Testing](https://github.com/buehler/dotnet-operator-sdk/workflows/Code%20Security%20Testing/badge.svg)
4-
![.NET Release](https://github.com/buehler/dotnet-operator-sdk/workflows/.NET%20Release/badge.svg)
5-
![.NET Testing](https://github.com/buehler/dotnet-operator-sdk/workflows/.NET%20Testing/badge.svg)
3+
[![.NET Pre-Release](https://github.com/buehler/dotnet-operator-sdk/actions/workflows/dotnet-release.yml/badge.svg?branch=main)](https://github.com/buehler/dotnet-operator-sdk/actions/workflows/dotnet-release.yml)
4+
[![.NET Release](https://github.com/buehler/dotnet-operator-sdk/actions/workflows/dotnet-release.yml/badge.svg?branch=release)](https://github.com/buehler/dotnet-operator-sdk/actions/workflows/dotnet-release.yml)
5+
[![Scheduled Code Security Testing](https://github.com/buehler/dotnet-operator-sdk/actions/workflows/security-analysis.yml/badge.svg?event=schedule)](https://github.com/buehler/dotnet-operator-sdk/actions/workflows/security-analysis.yml)
66

77
This is the repository of "KubeOps" - The dotnet Kubernetes Operator SDK.
88

examples/Operator/Entities/V1TestEntity.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ namespace Operator.Entities;
66
[KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")]
77
public class V1TestEntity : IKubernetesObject<V1ObjectMeta>, ISpec<V1TestEntitySpec>, IStatus<V1TestEntityStatus>
88
{
9-
public string ApiVersion { get; set; } = "testing.dev/v1";
9+
public required string ApiVersion { get; set; }
1010

11-
public string Kind { get; set; } = "TestEntity";
11+
public required string Kind { get; set; }
1212

1313
public V1ObjectMeta Metadata { get; set; } = new();
1414

src/KubeOps.Cli/Arguments.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace KubeOps.Cli;
44

55
internal static class Arguments
66
{
7-
public static readonly Argument<FileInfo> SolutionOrProjectFile = new(
7+
public static readonly Argument<FileInfo?> SolutionOrProjectFile = new(
88
"sln/csproj file",
99
() =>
1010
{
@@ -25,9 +25,7 @@ var slnFile
2525
{
2626
({ } prj, _) => prj,
2727
(_, { } sln) => sln,
28-
_ => throw new FileNotFoundException(
29-
"No *.csproj or *.sln file found in current directory.",
30-
Directory.GetCurrentDirectory()),
28+
_ => null,
3129
};
3230
},
3331
"A solution or project file where entities are located. " +

src/KubeOps.Cli/Commands/Generator/CrdGenerator.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,15 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
3838
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
3939
var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);
4040

41-
var parser = file.Extension switch
41+
var parser = file switch
4242
{
43-
".csproj" => await AssemblyParser.ForProject(console, file),
44-
".sln" => await AssemblyParser.ForSolution(
43+
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
44+
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
4545
console,
4646
file,
4747
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
4848
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
49+
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
4950
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
5051
};
5152
var result = new ResultOutput(console, format);
@@ -74,7 +75,5 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
7475
{
7576
result.Write();
7677
}
77-
78-
ctx.ExitCode = ExitCodes.Success;
7978
}
8079
}

src/KubeOps.Cli/Commands/Generator/RbacGenerator.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
3737
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
3838
var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);
3939

40-
var parser = file.Extension switch
40+
var parser = file switch
4141
{
42-
".csproj" => await AssemblyParser.ForProject(console, file),
43-
".sln" => await AssemblyParser.ForSolution(
42+
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
43+
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
4444
console,
4545
file,
4646
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
4747
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
48+
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
4849
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
4950
};
5051
var result = new ResultOutput(console, format);
@@ -59,7 +60,5 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
5960
{
6061
result.Write();
6162
}
62-
63-
ctx.ExitCode = ExitCodes.Success;
6463
}
6564
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.CommandLine;
2+
using System.CommandLine.Invocation;
3+
4+
using k8s;
5+
using k8s.Autorest;
6+
using k8s.Models;
7+
8+
using KubeOps.Cli.Roslyn;
9+
using KubeOps.Transpiler;
10+
11+
using Spectre.Console;
12+
13+
namespace KubeOps.Cli.Commands.Management;
14+
15+
internal static class Install
16+
{
17+
public static Command Command
18+
{
19+
get
20+
{
21+
var cmd =
22+
new Command("install", "Install CRDs into the cluster of the actually selected context.")
23+
{
24+
Options.Force,
25+
Options.SolutionProjectRegex,
26+
Options.TargetFramework,
27+
Arguments.SolutionOrProjectFile,
28+
};
29+
cmd.AddAlias("i");
30+
cmd.SetHandler(ctx => Handler(
31+
AnsiConsole.Console,
32+
new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()),
33+
ctx));
34+
35+
return cmd;
36+
}
37+
}
38+
39+
internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx)
40+
{
41+
var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
42+
var force = ctx.ParseResult.GetValueForOption(Options.Force);
43+
44+
var parser = file switch
45+
{
46+
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
47+
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
48+
console,
49+
file,
50+
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
51+
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
52+
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
53+
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
54+
};
55+
56+
console.WriteLine($"Install CRDs from {file.Name}.");
57+
var crds = Crds.Transpile(parser.Entities()).ToList();
58+
if (crds.Count == 0)
59+
{
60+
console.WriteLine("No CRDs found. Exiting.");
61+
ctx.ExitCode = ExitCodes.Success;
62+
return;
63+
}
64+
65+
console.WriteLine($"Found {crds.Count} CRDs.");
66+
console.WriteLine($"""Starting install into cluster with url "{client.BaseUri}".""");
67+
68+
foreach (var crd in crds)
69+
{
70+
console.MarkupLineInterpolated(
71+
$"""Install [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] into the cluster.""");
72+
73+
try
74+
{
75+
switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync(
76+
fieldSelector: $"metadata.name={crd.Name()}"))
77+
{
78+
case { Items: [var existing] }:
79+
console.MarkupLineInterpolated(
80+
$"""[yellow]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" already exists.[/]""");
81+
if (!force && console.Confirm("[yellow]Should the CRD be overwritten?[/]"))
82+
{
83+
ctx.ExitCode = ExitCodes.Aborted;
84+
return;
85+
}
86+
87+
crd.Metadata.ResourceVersion = existing.ResourceVersion();
88+
await client.ApiextensionsV1.ReplaceCustomResourceDefinitionAsync(crd, crd.Name());
89+
break;
90+
default:
91+
await client.ApiextensionsV1.CreateCustomResourceDefinitionAsync(crd);
92+
break;
93+
}
94+
95+
console.MarkupLineInterpolated(
96+
$"""[green]Installed / Updated CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
97+
}
98+
catch (HttpOperationException)
99+
{
100+
console.WriteLine(
101+
$"""[red]There was a http (api) error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
102+
throw;
103+
}
104+
catch (Exception)
105+
{
106+
console.WriteLine(
107+
$"""[red]There was an error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
108+
throw;
109+
}
110+
}
111+
}
112+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.CommandLine;
2+
using System.CommandLine.Invocation;
3+
4+
using k8s;
5+
using k8s.Autorest;
6+
using k8s.Models;
7+
8+
using KubeOps.Cli.Roslyn;
9+
using KubeOps.Transpiler;
10+
11+
using Spectre.Console;
12+
13+
namespace KubeOps.Cli.Commands.Management;
14+
15+
internal static class Uninstall
16+
{
17+
public static Command Command
18+
{
19+
get
20+
{
21+
var cmd =
22+
new Command("uninstall", "Uninstall CRDs from the cluster of the actually selected context.")
23+
{
24+
Options.Force,
25+
Options.SolutionProjectRegex,
26+
Options.TargetFramework,
27+
Arguments.SolutionOrProjectFile,
28+
};
29+
cmd.AddAlias("u");
30+
cmd.SetHandler(ctx => Handler(
31+
AnsiConsole.Console,
32+
new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()),
33+
ctx));
34+
35+
return cmd;
36+
}
37+
}
38+
39+
internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx)
40+
{
41+
var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
42+
var force = ctx.ParseResult.GetValueForOption(Options.Force);
43+
44+
var parser = file switch
45+
{
46+
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
47+
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
48+
console,
49+
file,
50+
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
51+
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
52+
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
53+
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
54+
};
55+
56+
console.WriteLine($"Uninstall CRDs from {file.Name}.");
57+
var crds = Crds.Transpile(parser.Entities()).ToList();
58+
if (crds.Count == 0)
59+
{
60+
console.WriteLine("No CRDs found. Exiting.");
61+
ctx.ExitCode = ExitCodes.Success;
62+
return;
63+
}
64+
65+
console.WriteLine($"Found {crds.Count} CRDs.");
66+
if (!force && !console.Confirm("[red]Should the CRDs be uninstalled?[/]", false))
67+
{
68+
ctx.ExitCode = ExitCodes.Aborted;
69+
return;
70+
}
71+
72+
console.WriteLine($"""Starting uninstall from cluster with url "{client.BaseUri}".""");
73+
74+
foreach (var crd in crds)
75+
{
76+
console.MarkupLineInterpolated(
77+
$"""Uninstall [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] from the cluster.""");
78+
79+
try
80+
{
81+
switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync(
82+
fieldSelector: $"metadata.name={crd.Name()}"))
83+
{
84+
case { Items: [var existing] }:
85+
await client.ApiextensionsV1.DeleteCustomResourceDefinitionAsync(existing.Name());
86+
console.MarkupLineInterpolated(
87+
$"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" deleted.[/]""");
88+
break;
89+
default:
90+
console.MarkupLineInterpolated(
91+
$"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" did not exist.[/]""");
92+
break;
93+
}
94+
}
95+
catch (HttpOperationException)
96+
{
97+
console.WriteLine(
98+
$"""[red]There was a http (api) error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
99+
throw;
100+
}
101+
catch (Exception)
102+
{
103+
console.WriteLine(
104+
$"""[red]There was an error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
105+
throw;
106+
}
107+
}
108+
}
109+
}

src/KubeOps.Cli/ExitCodes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ internal static class ExitCodes
44
{
55
public const int Success = 0;
66
public const int Error = 1;
7+
public const int Aborted = 2;
78
public const int UsageError = 99;
89
}

src/KubeOps.Cli/Options.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,9 @@ internal static class Options
3030
},
3131
description: "Regex pattern to filter projects in the solution to search for entities. " +
3232
"If omitted, all projects are searched.");
33+
34+
public static readonly Option<bool> Force = new(
35+
new[] { "--force", "-f" },
36+
() => false,
37+
description: "Do not bother the user with questions and just do it.");
3338
}

src/KubeOps.Cli/Output/ResultOutput.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void Write()
3939
_console.Write(new Rule());
4040
foreach (var (filename, content) in _files)
4141
{
42-
_console.MarkupLine($"[bold]File:[/] [underline]{filename}[/]");
42+
_console.MarkupLineInterpolated($"[bold]File:[/] [underline]{filename}[/]");
4343
_console.WriteLine(Serialize(content));
4444
_console.Write(new Rule());
4545
}

0 commit comments

Comments
 (0)