Skip to content

Commit

Permalink
feat(cli): management install/uninstall commands (#618)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
buehler authored Sep 28, 2023
1 parent 34b3dcf commit 7cac2ad
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 34 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# KubeOps

![Code Security Testing](https://github.com/buehler/dotnet-operator-sdk/workflows/Code%20Security%20Testing/badge.svg)
![.NET Release](https://github.com/buehler/dotnet-operator-sdk/workflows/.NET%20Release/badge.svg)
![.NET Testing](https://github.com/buehler/dotnet-operator-sdk/workflows/.NET%20Testing/badge.svg)
[![.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)
[![.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)
[![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)

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

Expand Down
4 changes: 2 additions & 2 deletions examples/Operator/Entities/V1TestEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace Operator.Entities;
[KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")]
public class V1TestEntity : IKubernetesObject<V1ObjectMeta>, ISpec<V1TestEntitySpec>, IStatus<V1TestEntityStatus>
{
public string ApiVersion { get; set; } = "testing.dev/v1";
public required string ApiVersion { get; set; }

public string Kind { get; set; } = "TestEntity";
public required string Kind { get; set; }

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

Expand Down
6 changes: 2 additions & 4 deletions src/KubeOps.Cli/Arguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace KubeOps.Cli;

internal static class Arguments
{
public static readonly Argument<FileInfo> SolutionOrProjectFile = new(
public static readonly Argument<FileInfo?> SolutionOrProjectFile = new(
"sln/csproj file",
() =>
{
Expand All @@ -25,9 +25,7 @@ var slnFile
{
({ } prj, _) => prj,
(_, { } sln) => sln,
_ => throw new FileNotFoundException(
"No *.csproj or *.sln file found in current directory.",
Directory.GetCurrentDirectory()),
_ => null,
};
},
"A solution or project file where entities are located. " +
Expand Down
9 changes: 4 additions & 5 deletions src/KubeOps.Cli/Commands/Generator/CrdGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);

var parser = file.Extension switch
var parser = file switch
{
".csproj" => await AssemblyParser.ForProject(console, file),
".sln" => await AssemblyParser.ForSolution(
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
console,
file,
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
};
var result = new ResultOutput(console, format);
Expand Down Expand Up @@ -74,7 +75,5 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
{
result.Write();
}

ctx.ExitCode = ExitCodes.Success;
}
}
9 changes: 4 additions & 5 deletions src/KubeOps.Cli/Commands/Generator/RbacGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
var outPath = ctx.ParseResult.GetValueForOption(Options.OutputPath);
var format = ctx.ParseResult.GetValueForOption(Options.OutputFormat);

var parser = file.Extension switch
var parser = file switch
{
".csproj" => await AssemblyParser.ForProject(console, file),
".sln" => await AssemblyParser.ForSolution(
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
console,
file,
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
};
var result = new ResultOutput(console, format);
Expand All @@ -59,7 +60,5 @@ internal static async Task Handler(IAnsiConsole console, InvocationContext ctx)
{
result.Write();
}

ctx.ExitCode = ExitCodes.Success;
}
}
112 changes: 112 additions & 0 deletions src/KubeOps.Cli/Commands/Management/Install.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.CommandLine;
using System.CommandLine.Invocation;

using k8s;
using k8s.Autorest;
using k8s.Models;

using KubeOps.Cli.Roslyn;
using KubeOps.Transpiler;

using Spectre.Console;

namespace KubeOps.Cli.Commands.Management;

internal static class Install
{
public static Command Command
{
get
{
var cmd =
new Command("install", "Install CRDs into the cluster of the actually selected context.")
{
Options.Force,
Options.SolutionProjectRegex,
Options.TargetFramework,
Arguments.SolutionOrProjectFile,
};
cmd.AddAlias("i");
cmd.SetHandler(ctx => Handler(
AnsiConsole.Console,
new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()),
ctx));

return cmd;
}
}

internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx)
{
var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
var force = ctx.ParseResult.GetValueForOption(Options.Force);

var parser = file switch
{
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
console,
file,
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
};

console.WriteLine($"Install CRDs from {file.Name}.");
var crds = Crds.Transpile(parser.Entities()).ToList();
if (crds.Count == 0)
{
console.WriteLine("No CRDs found. Exiting.");
ctx.ExitCode = ExitCodes.Success;
return;
}

console.WriteLine($"Found {crds.Count} CRDs.");
console.WriteLine($"""Starting install into cluster with url "{client.BaseUri}".""");

foreach (var crd in crds)
{
console.MarkupLineInterpolated(
$"""Install [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] into the cluster.""");

try
{
switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync(
fieldSelector: $"metadata.name={crd.Name()}"))
{
case { Items: [var existing] }:
console.MarkupLineInterpolated(
$"""[yellow]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" already exists.[/]""");
if (!force && console.Confirm("[yellow]Should the CRD be overwritten?[/]"))
{
ctx.ExitCode = ExitCodes.Aborted;
return;
}

crd.Metadata.ResourceVersion = existing.ResourceVersion();
await client.ApiextensionsV1.ReplaceCustomResourceDefinitionAsync(crd, crd.Name());
break;
default:
await client.ApiextensionsV1.CreateCustomResourceDefinitionAsync(crd);
break;
}

console.MarkupLineInterpolated(
$"""[green]Installed / Updated CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
}
catch (HttpOperationException)
{
console.WriteLine(
$"""[red]There was a http (api) error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
throw;
}
catch (Exception)
{
console.WriteLine(
$"""[red]There was an error while installing "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
throw;
}
}
}
}
109 changes: 109 additions & 0 deletions src/KubeOps.Cli/Commands/Management/Uninstall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System.CommandLine;
using System.CommandLine.Invocation;

using k8s;
using k8s.Autorest;
using k8s.Models;

using KubeOps.Cli.Roslyn;
using KubeOps.Transpiler;

using Spectre.Console;

namespace KubeOps.Cli.Commands.Management;

internal static class Uninstall
{
public static Command Command
{
get
{
var cmd =
new Command("uninstall", "Uninstall CRDs from the cluster of the actually selected context.")
{
Options.Force,
Options.SolutionProjectRegex,
Options.TargetFramework,
Arguments.SolutionOrProjectFile,
};
cmd.AddAlias("u");
cmd.SetHandler(ctx => Handler(
AnsiConsole.Console,
new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()),
ctx));

return cmd;
}
}

internal static async Task Handler(IAnsiConsole console, IKubernetes client, InvocationContext ctx)
{
var file = ctx.ParseResult.GetValueForArgument(Arguments.SolutionOrProjectFile);
var force = ctx.ParseResult.GetValueForOption(Options.Force);

var parser = file switch
{
{ Extension: ".csproj", Exists: true } => await AssemblyParser.ForProject(console, file),
{ Extension: ".sln", Exists: true } => await AssemblyParser.ForSolution(
console,
file,
ctx.ParseResult.GetValueForOption(Options.SolutionProjectRegex),
ctx.ParseResult.GetValueForOption(Options.TargetFramework)),
{ Exists: false } => throw new FileNotFoundException($"The file {file.Name} does not exist."),
_ => throw new NotSupportedException("Only *.csproj and *.sln files are supported."),
};

console.WriteLine($"Uninstall CRDs from {file.Name}.");
var crds = Crds.Transpile(parser.Entities()).ToList();
if (crds.Count == 0)
{
console.WriteLine("No CRDs found. Exiting.");
ctx.ExitCode = ExitCodes.Success;
return;
}

console.WriteLine($"Found {crds.Count} CRDs.");
if (!force && !console.Confirm("[red]Should the CRDs be uninstalled?[/]", false))
{
ctx.ExitCode = ExitCodes.Aborted;
return;
}

console.WriteLine($"""Starting uninstall from cluster with url "{client.BaseUri}".""");

foreach (var crd in crds)
{
console.MarkupLineInterpolated(
$"""Uninstall [cyan]"{crd.Spec.Group}/{crd.Spec.Names.Kind}"[/] from the cluster.""");

try
{
switch (await client.ApiextensionsV1.ListCustomResourceDefinitionAsync(
fieldSelector: $"metadata.name={crd.Name()}"))
{
case { Items: [var existing] }:
await client.ApiextensionsV1.DeleteCustomResourceDefinitionAsync(existing.Name());
console.MarkupLineInterpolated(
$"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" deleted.[/]""");
break;
default:
console.MarkupLineInterpolated(
$"""[green]CRD "{crd.Spec.Group}/{crd.Spec.Names.Kind}" did not exist.[/]""");
break;
}
}
catch (HttpOperationException)
{
console.WriteLine(
$"""[red]There was a http (api) error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
throw;
}
catch (Exception)
{
console.WriteLine(
$"""[red]There was an error while uninstalling "{crd.Spec.Group}/{crd.Spec.Names.Kind}".[/]""");
throw;
}
}
}
}
1 change: 1 addition & 0 deletions src/KubeOps.Cli/ExitCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ internal static class ExitCodes
{
public const int Success = 0;
public const int Error = 1;
public const int Aborted = 2;
public const int UsageError = 99;
}
5 changes: 5 additions & 0 deletions src/KubeOps.Cli/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ internal static class Options
},
description: "Regex pattern to filter projects in the solution to search for entities. " +
"If omitted, all projects are searched.");

public static readonly Option<bool> Force = new(
new[] { "--force", "-f" },
() => false,
description: "Do not bother the user with questions and just do it.");
}
2 changes: 1 addition & 1 deletion src/KubeOps.Cli/Output/ResultOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void Write()
_console.Write(new Rule());
foreach (var (filename, content) in _files)
{
_console.MarkupLine($"[bold]File:[/] [underline]{filename}[/]");
_console.MarkupLineInterpolated($"[bold]File:[/] [underline]{filename}[/]");
_console.WriteLine(Serialize(content));
_console.Write(new Rule());
}
Expand Down
10 changes: 6 additions & 4 deletions src/KubeOps.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@

using KubeOps.Cli;
using KubeOps.Cli.Commands.Generator;
using KubeOps.Cli.Commands.Management;

using Spectre.Console;

using Version = KubeOps.Cli.Commands.Utilities.Version;

await new CommandLineBuilder(new RootCommand(
return await new CommandLineBuilder(new RootCommand(
"CLI for KubeOps. Commandline tool to help with management tasks such as generating or installing CRDs.")
{
Generator.Command, Version.Command,
Generator.Command, Version.Command, Install.Command, Uninstall.Command,
})
.UseDefaults()
.UseParseErrorReporting(ExitCodes.UsageError)
.UseExceptionHandler((ex, ctx) =>
{
AnsiConsole.MarkupLine($"[red]An error ocurred whiled executing {ctx.ParseResult.CommandResult.Command}[/]");
AnsiConsole.WriteException(ex);
AnsiConsole.MarkupLineInterpolated($"[red]An error occurred whiled executing {ctx.ParseResult.CommandResult.Command}[/]");
AnsiConsole.MarkupLineInterpolated($"[red]{ex.Message}[/]");
ctx.ExitCode = ExitCodes.Error;
})
.Build()
.InvokeAsync(args);
Loading

0 comments on commit 7cac2ad

Please sign in to comment.