Browse Source

Implement undeploy command (#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.
pull/393/head
Ryan Nowak 6 years ago
committed by GitHub
parent
commit
1178d1d9e1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      docs/reference/commandline/tye-deploy.md
  2. 70
      docs/reference/commandline/tye-undeploy.md
  3. 7
      docs/reference/commandline/tye.md
  4. 4
      src/Microsoft.Tye.Core/ValidateSecretStep.cs
  5. 45
      src/tye/Program.UndeployCommand.cs
  6. 1
      src/tye/Program.cs
  7. 220
      src/tye/UndeployHost.cs

2
docs/reference/commandline/tye-deploy.md

@ -19,6 +19,8 @@ The `tye deploy` command will deploy an application to Kubernetes. `tye deploy`
- Generate a Kubernetes Deployment and Service for each project.
- Apply the generated Deployment and Service to your current Kubernetes context.
`tye deploy` operates in the current Kubernetes namespace. Use `kubectl config view --minify --output 'jsonpath={..namespace}'` to view the current namespace.
## Arguments
`PATH`

70
docs/reference/commandline/tye-undeploy.md

@ -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
```

7
docs/reference/commandline/tye.md

@ -29,6 +29,7 @@ For example, [`tye run`](tye-run.md) runs an application. Each command defines i
| Command | Function |
| --------------------------------------------- | ------------------------------------------------------------------- |
| [tye init](tye-init.md) | Scaffolds a tye.yaml file representing the application. |
| [tye run](tye-run.md) | Runs an application. |
| [tye deploy](tye-deploy.md) | Deploys an application. |
| [tye init](tye-init.md) | Scaffolds a tye.yaml file representing the application. |
| [tye run](tye-run.md) | Runs an application. |
| [tye deploy](tye-deploy.md) | Deploys an application. |
| [tye undeploy](tye-undeploy.md) | Deletes a deployed application. |

4
src/Microsoft.Tye.Core/ValidateSecretStep.cs

@ -161,6 +161,10 @@ namespace Microsoft.Tye
secret.Metadata = new V1ObjectMeta();
secret.Metadata.Name = secretInputBinding.Name;
secret.Metadata.Labels = new Dictionary<string, string>()
{
["app.kubernetes.io/part-of"] = application.Name,
};
output.WriteDebugLine($"Creating secret '{secret.Metadata.Name}'.");

45
src/tye/Program.UndeployCommand.cs

@ -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;
}
}
}

1
src/tye/Program.cs

@ -29,6 +29,7 @@ namespace Microsoft.Tye
command.AddCommand(CreateBuildCommand());
command.AddCommand(CreatePushCommand());
command.AddCommand(CreateDeployCommand());
command.AddCommand(CreateUndeployCommand());
// Show commandline help unless a subcommand was used.
command.Handler = CommandHandler.Create<IHelpBuilder>(help =>

220
src/tye/UndeployHost.cs

@ -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…
Cancel
Save