mirror of https://github.com/dotnet/tye.git
Browse Source
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.pull/393/head
committed by
GitHub
7 changed files with 346 additions and 3 deletions
@ -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 |
|||
``` |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue