forked from dotnet/tye
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement undeploy command (dotnet#385)
This is new command that can be used to delete everything running in a cluster related to an application using the labels we use. This currently uses a hardcoded set of resource types. It would be possible to make this generic, which might become needed in the future. See comments in the code. --- The overall design for how to use the command: ```sh tye undeploy ``` Will read a solution or tye.yaml to get the application name. It then enumerates and deletes all resources in the cluster with `app.kubernetes.io/part-of=application-name` (in the current namespace). ```sh tye undeploy --what-if ``` Will do the same, but without actually doing the deletion. ```sh tye undeploy -i ``` Will interactively give you a yes/no choice for whether to delete each resource.
- Loading branch information
1 parent
2b515c3
commit 10f2a27
Showing
7 changed files
with
346 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# tye undeploy | ||
|
||
## Name | ||
|
||
`tye undeploy` - Removes a deployed application from Kubernetes. | ||
|
||
## Synopsis | ||
|
||
```text | ||
tye undeploy [-?|-h|--help] [-i|--interactive] [-v|--verbosity <Debug|Info|Quiet>] [--what-if] [<PATH>] | ||
``` | ||
|
||
## Description | ||
|
||
The `tye undeploy` command will delete a deployed application from Kubernetes. `tye undeploy` by default will: | ||
|
||
- List all resources that are part of an application | ||
- Print the list of resources (what-if) | ||
- Offer a choice to delete each resource (interactive) | ||
- Delete each resource (if applicable) | ||
|
||
`tye undeploy` operates in the current Kubernetes namespace. Use `kubectl config view --minify --output 'jsonpath={..namespace}'` to view the current namespace. | ||
|
||
Undeploy decides which resources to delete based on the `app.kubernetes.io/part-of=...` label. This label will be set to the application name for all resources created by Tye. `tye undeploy` does not rely on the list of services in `tye.yaml` or a solution file. | ||
|
||
## Arguments | ||
|
||
`PATH` | ||
|
||
The path to either a file or directory to execute `tye undeploy` on. Can either be a yaml, sln, or project file, however it is recommend to have a tye.yaml file for `tye undeploy`. | ||
|
||
If a directory path is specified, `tye undeploy` will default to using these files, in the following order: | ||
|
||
- `tye.yaml` | ||
- `*.sln` | ||
- `*.csproj/*.fsproj` | ||
|
||
## Options | ||
|
||
- `-i|--interactive` | ||
|
||
Does an interactive undeploy that will prompt for deletion of each resource. | ||
|
||
- `-v|--verbosity <Debug|Info|Quiet>` | ||
|
||
The verbosity of logs emitted by `tye undeploy`. Defaults to Info. | ||
|
||
- `--what-if` | ||
|
||
Print each resource instead of deleting. | ||
|
||
## Examples | ||
|
||
- Delete a deployed application from the current directory: | ||
|
||
```text | ||
tye undeploy | ||
``` | ||
|
||
- Delete a deployed application with interactive input: | ||
|
||
```text | ||
tye undeploy --interactive | ||
``` | ||
|
||
- Display the resources that would be deleted by an undeploy operation: | ||
|
||
```text | ||
tye undeploy --what-if | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Collections.Generic; | ||
using System.CommandLine; | ||
using System.CommandLine.Invocation; | ||
using System.IO; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Microsoft.Tye.ConfigModel; | ||
|
||
namespace Microsoft.Tye | ||
{ | ||
static partial class Program | ||
{ | ||
public static Command CreateUndeployCommand() | ||
{ | ||
var command = new Command("undeploy", "delete deployed application") | ||
{ | ||
CommonArguments.Path_Required, | ||
StandardOptions.Interactive, | ||
StandardOptions.Verbosity, | ||
|
||
new Option(new[]{ "--what-if", }, "print what would be deleted without making changes") | ||
{ | ||
Argument = new Argument<bool>(), | ||
}, | ||
}; | ||
|
||
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool>((console, path, verbosity, interactive, whatIf) => | ||
{ | ||
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 | ||
if (path is null) | ||
{ | ||
throw new CommandException("No project or solution file was found."); | ||
} | ||
return UndeployHost.UndeployAsync(console, path, verbosity, interactive, whatIf); | ||
}); | ||
|
||
return command; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.CommandLine; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using k8s; | ||
using k8s.Models; | ||
using Microsoft.Rest; | ||
using Microsoft.Tye.ConfigModel; | ||
using Newtonsoft.Json; | ||
|
||
namespace Microsoft.Tye | ||
{ | ||
public static class UndeployHost | ||
{ | ||
public static async Task UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, bool whatIf) | ||
{ | ||
var output = new OutputContext(console, verbosity); | ||
|
||
output.WriteInfoLine("Loading Application Details..."); | ||
|
||
// We don't need to know anything about the services, just the application name. | ||
var application = ConfigFactory.FromFile(path); | ||
|
||
await ExecuteUndeployAsync(output, application, interactive, whatIf); | ||
} | ||
|
||
public static async Task ExecuteUndeployAsync(OutputContext output, ConfigApplication application, bool interactive, bool whatIf) | ||
{ | ||
var config = KubernetesClientConfiguration.BuildDefaultConfig(); | ||
|
||
// Workaround for https://github.com/kubernetes-client/csharp/issues/372 | ||
var store = await KubernetesClientConfiguration.LoadKubeConfigAsync(); | ||
var context = store.Contexts.Where(c => c.Name == config.CurrentContext).FirstOrDefault(); | ||
config.Namespace ??= context?.ContextDetails?.Namespace; | ||
|
||
var kubernetes = new Kubernetes(config); | ||
|
||
// Due to some limitations in the k8s SDK we currently have a hardcoded list of resource | ||
// types that we handle deletes for. If we start adding extensibility for the *kinds* of | ||
// k8s resources we create, or the ability to deploy additional files along with the | ||
// resources we understand then we should revisit this. | ||
// | ||
// Basically the challenges are: | ||
// | ||
// - kubectl api-resources --all (and similar) are implemented client-side (n+1 problem) | ||
// - the C# k8s SDK doesn't have an untyped api for operations on arbitrary resources, the | ||
// closest thing is the custom resource APIs | ||
// - Legacy resources without an api group don't follow the same URL scheme as more modern | ||
// ones, and thus cannot be addressed using the custom resource APIs. | ||
// | ||
// So solving 'undeploy' generically would involve doing a bunch of work to query things | ||
// generically, including going outside of what's provided by the SDK. | ||
// | ||
// - querying api-resources | ||
// - querying api-groups | ||
// - handcrafing requests to list for each resource | ||
// - handcrafting requests to delete each resource | ||
var resources = new List<Resource>(); | ||
|
||
try | ||
{ | ||
output.WriteDebugLine("Querying services"); | ||
var response = await kubernetes.ListNamespacedServiceWithHttpMessagesAsync( | ||
config.Namespace, | ||
labelSelector: $"app.kubernetes.io/part-of={application.Name}"); | ||
|
||
foreach (var resource in response.Body.Items) | ||
{ | ||
resource.Kind = V1Service.KubeKind; | ||
} | ||
|
||
resources.AddRange(response.Body.Items.Select(item => new Resource(item, item.Metadata, DeleteService))); | ||
output.WriteDebugLine($"Found {response.Body.Items.Count} matching services"); | ||
} | ||
catch (Exception ex) | ||
{ | ||
output.WriteDebugLine("Failed to query services."); | ||
output.WriteDebugLine(ex.ToString()); | ||
throw new CommandException("Unable connect to kubernetes.", ex); | ||
} | ||
|
||
try | ||
{ | ||
output.WriteDebugLine("Querying deployments"); | ||
var response = await kubernetes.ListNamespacedDeploymentWithHttpMessagesAsync( | ||
config.Namespace, | ||
labelSelector: $"app.kubernetes.io/part-of={application.Name}"); | ||
|
||
foreach (var resource in response.Body.Items) | ||
{ | ||
resource.Kind = V1Deployment.KubeKind; | ||
} | ||
|
||
resources.AddRange(response.Body.Items.Select(item => new Resource(item, item.Metadata, DeleteDeployment))); | ||
output.WriteDebugLine($"Found {response.Body.Items.Count} matching deployments"); | ||
} | ||
catch (Exception ex) | ||
{ | ||
output.WriteDebugLine("Failed to query deployments."); | ||
output.WriteDebugLine(ex.ToString()); | ||
throw new CommandException("Unable connect to kubernetes.", ex); | ||
} | ||
|
||
try | ||
{ | ||
output.WriteDebugLine("Querying secrets"); | ||
var response = await kubernetes.ListNamespacedSecretWithHttpMessagesAsync( | ||
config.Namespace, | ||
labelSelector: $"app.kubernetes.io/part-of={application.Name}"); | ||
|
||
foreach (var resource in response.Body.Items) | ||
{ | ||
resource.Kind = V1Secret.KubeKind; | ||
} | ||
|
||
resources.AddRange(response.Body.Items.Select(item => new Resource(item, item.Metadata, DeleteSecret))); | ||
output.WriteDebugLine($"Found {response.Body.Items.Count} matching secrets"); | ||
|
||
} | ||
catch (Exception ex) | ||
{ | ||
output.WriteDebugLine("Failed to query secrets."); | ||
output.WriteDebugLine(ex.ToString()); | ||
throw new CommandException("Unable connect to kubernetes.", ex); | ||
} | ||
|
||
output.WriteInfoLine($"Found {resources.Count} resource(s)."); | ||
|
||
var exceptions = new List<(Resource resource, HttpOperationException exception)>(); | ||
foreach (var resource in resources) | ||
{ | ||
var operation = Operations.Delete; | ||
if (interactive && !output.Confirm($"Delete {resource.Obj.Kind} '{resource.Metadata.Name}'?")) | ||
{ | ||
operation = Operations.None; | ||
} | ||
|
||
if (whatIf && operation == Operations.Delete) | ||
{ | ||
operation = Operations.Explain; | ||
} | ||
|
||
if (operation == Operations.None) | ||
{ | ||
output.WriteAlwaysLine($"Skipping '{resource.Obj.Kind}' '{resource.Metadata.Name}' ..."); | ||
} | ||
else if (operation == Operations.Explain) | ||
{ | ||
output.WriteAlwaysLine($"whatif: Deleting '{resource.Obj.Kind}' '{resource.Metadata.Name}' ..."); | ||
} | ||
else if (operation == Operations.Delete) | ||
{ | ||
output.WriteAlwaysLine($"Deleting '{resource.Obj.Kind}' '{resource.Metadata.Name}' ..."); | ||
|
||
try | ||
{ | ||
var response = await resource.Deleter(resource.Metadata.Name); | ||
|
||
output.WriteDebugLine($"Successfully deleted resource: '{resource.Obj.Kind}' '{resource.Metadata.Name}'."); | ||
} | ||
catch (HttpOperationException ex) | ||
{ | ||
output.WriteDebugLine($"Failed to delete resource: '{resource.Obj.Kind}' '{resource.Metadata.Name}'."); | ||
output.WriteDebugLine(ex.ToString()); | ||
exceptions.Add((resource, ex)); | ||
} | ||
} | ||
} | ||
|
||
if (exceptions.Count > 0) | ||
{ | ||
throw new CommandException( | ||
$"Failed to delete some resources: " + Environment.NewLine + Environment.NewLine + | ||
string.Join(Environment.NewLine, exceptions.Select(e => $"\t'{e.resource.Obj.Kind}' '{e.resource.Metadata.Name}': {e.exception.Body}."))); | ||
} | ||
|
||
Task<Rest.HttpOperationResponse<V1Status>> DeleteService(string name) | ||
{ | ||
return kubernetes!.DeleteNamespacedServiceWithHttpMessagesAsync(name, config!.Namespace); | ||
} | ||
|
||
Task<Rest.HttpOperationResponse<V1Status>> DeleteDeployment(string name) | ||
{ | ||
return kubernetes!.DeleteNamespacedDeploymentWithHttpMessagesAsync(name, config!.Namespace); | ||
} | ||
|
||
Task<Rest.HttpOperationResponse<V1Status>> DeleteSecret(string name) | ||
{ | ||
return kubernetes!.DeleteNamespacedSecretWithHttpMessagesAsync(name, config!.Namespace); | ||
} | ||
} | ||
|
||
private enum Operations | ||
{ | ||
None, | ||
Delete, | ||
Explain, | ||
} | ||
|
||
private readonly struct Resource | ||
{ | ||
public readonly IKubernetesObject Obj; | ||
public readonly V1ObjectMeta Metadata; | ||
public readonly Func<string, Task<Rest.HttpOperationResponse<V1Status>>> Deleter; | ||
|
||
public Resource(IKubernetesObject obj, V1ObjectMeta metadata, Func<string, Task<Rest.HttpOperationResponse<V1Status>>> deleter) | ||
{ | ||
Obj = obj; | ||
Metadata = metadata; | ||
Deleter = deleter; | ||
} | ||
} | ||
} | ||
} |