From 66a86301019f7c8dc104219c0b7d1e805d308899 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 30 Aug 2021 01:36:42 +0200 Subject: [PATCH] feat: implement undeploy command --- TODO.md | 3 +- cli/app/undeploy.go | 46 +++++++++++++- client/swarm/common.go | 50 +++++++++++++++ client/swarm/remove.go | 139 +++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 4 ++ 6 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 client/swarm/common.go create mode 100644 client/swarm/remove.go diff --git a/TODO.md b/TODO.md index 9246e3faa..1c437e812 100644 --- a/TODO.md +++ b/TODO.md @@ -30,7 +30,8 @@ - [ ] `generate` - [ ] `insert` - [ ] `rm` - - [ ] `undeploy` + - [ ] `ls` + - [x] `undeploy` - [ ] `volume` - [ ] `ls` (WIP: knoflook) - [ ] `rm` (WIP: knoflook) diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index a4177c2b7..0a18ccacc 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -1,7 +1,49 @@ package app -import "github.com/urfave/cli/v2" +import ( + "context" + "errors" + + "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/client" + "coopcloud.tech/abra/client/swarm" + "coopcloud.tech/abra/config" + "github.com/docker/cli/cli/command/stack/options" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) var appUndeployCommand = &cli.Command{ - Name: "undeploy", + Name: "undeploy", + Usage: "Undeploy an app", + Action: func(c *cli.Context) error { + appName := c.Args().First() + if appName == "" { + internal.ShowSubcommandHelpAndError(c, errors.New("no app name provided")) + } + + appFiles, err := config.LoadAppFiles("") + if err != nil { + logrus.Fatal(err) + } + + appEnv, err := config.GetApp(appFiles, appName) + if err != nil { + logrus.Fatal(err) + } + + ctx := context.Background() + host := appFiles[appName].Server + cl, err := client.NewClientWithContext(host) + if err != nil { + logrus.Fatal(err) + } + + rmOpts := options.Remove{Namespaces: []string{appEnv.StackName()}} + if err := swarm.RunRemove(ctx, cl, rmOpts); err != nil { + logrus.Fatal(err) + } + + return nil + }, } diff --git a/client/swarm/common.go b/client/swarm/common.go new file mode 100644 index 000000000..b4193df36 --- /dev/null +++ b/client/swarm/common.go @@ -0,0 +1,50 @@ +package swarm + +import ( + "context" + + "github.com/docker/cli/cli/compose/convert" + "github.com/docker/cli/opts" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +func getStackFilter(namespace string) filters.Args { + filter := filters.NewArgs() + filter.Add("label", convert.LabelNamespace+"="+namespace) + return filter +} + +func getStackServiceFilter(namespace string) filters.Args { + return getStackFilter(namespace) +} + +func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args { + filter := opt.Value() + filter.Add("label", convert.LabelNamespace+"="+namespace) + return filter +} + +func getAllStacksFilter() filters.Args { + filter := filters.NewArgs() + filter.Add("label", convert.LabelNamespace) + return filter +} + +func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) { + return apiclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)}) +} + +func getStackNetworks(ctx context.Context, apiclient client.APIClient, namespace string) ([]types.NetworkResource, error) { + return apiclient.NetworkList(ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)}) +} + +func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Secret, error) { + return apiclient.SecretList(ctx, types.SecretListOptions{Filters: getStackFilter(namespace)}) +} + +func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) { + return apiclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)}) +} diff --git a/client/swarm/remove.go b/client/swarm/remove.go new file mode 100644 index 000000000..6a0b3fd87 --- /dev/null +++ b/client/swarm/remove.go @@ -0,0 +1,139 @@ +package swarm + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/docker/cli/cli/command/stack/options" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/api/types/versions" + apiclient "github.com/docker/docker/client" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// RunRemove is the swarm implementation of docker stack remove +func RunRemove(ctx context.Context, client *apiclient.Client, opts options.Remove) error { + var errs []string + for _, namespace := range opts.Namespaces { + services, err := getStackServices(ctx, client, namespace) + if err != nil { + return err + } + + networks, err := getStackNetworks(ctx, client, namespace) + if err != nil { + return err + } + + var secrets []swarm.Secret + if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") { + secrets, err = getStackSecrets(ctx, client, namespace) + if err != nil { + return err + } + } + + var configs []swarm.Config + if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") { + configs, err = getStackConfigs(ctx, client, namespace) + if err != nil { + return err + } + } + + if len(services)+len(networks)+len(secrets)+len(configs) == 0 { + logrus.Warning(fmt.Errorf("nothing found in stack: %s", namespace)) + continue + } + + hasError := removeServices(ctx, client, services) + hasError = removeSecrets(ctx, client, secrets) || hasError + hasError = removeConfigs(ctx, client, configs) || hasError + hasError = removeNetworks(ctx, client, networks) || hasError + + if hasError { + errs = append(errs, fmt.Sprintf("failed to remove some resources from stack: %s", namespace)) + } + } + + if len(errs) > 0 { + return errors.Errorf(strings.Join(errs, "\n")) + } + + return nil +} + +func sortServiceByName(services []swarm.Service) func(i, j int) bool { + return func(i, j int) bool { + return services[i].Spec.Name < services[j].Spec.Name + } +} + +func removeServices( + ctx context.Context, + client *apiclient.Client, + services []swarm.Service, +) bool { + var hasError bool + sort.Slice(services, sortServiceByName(services)) + for _, service := range services { + logrus.Infof("removing service %s\n", service.Spec.Name) + if err := client.ServiceRemove(ctx, service.ID); err != nil { + hasError = true + logrus.Fatalf("failed to remove service %s: %s", service.ID, err) + } + } + return hasError +} + +func removeNetworks( + ctx context.Context, + client *apiclient.Client, + networks []types.NetworkResource, +) bool { + var hasError bool + for _, network := range networks { + logrus.Infof("removing network %s\n", network.Name) + if err := client.NetworkRemove(ctx, network.ID); err != nil { + hasError = true + logrus.Fatalf("failed to remove network %s: %s", network.ID, err) + } + } + return hasError +} + +func removeSecrets( + ctx context.Context, + client *apiclient.Client, + secrets []swarm.Secret, +) bool { + var hasError bool + for _, secret := range secrets { + logrus.Infof("Removing secret %s\n", secret.Spec.Name) + if err := client.SecretRemove(ctx, secret.ID); err != nil { + hasError = true + logrus.Fatalf("Failed to remove secret %s: %s", secret.ID, err) + } + } + return hasError +} + +func removeConfigs( + ctx context.Context, + client *apiclient.Client, + configs []swarm.Config, +) bool { + var hasError bool + for _, config := range configs { + logrus.Infof("removing config %s\n", config.Spec.Name) + if err := client.ConfigRemove(ctx, config.ID); err != nil { + hasError = true + logrus.Fatalf("failed to remove config %s: %s", config.ID, err) + } + } + return hasError +} diff --git a/go.mod b/go.mod index 10421f8e8..0b53edc25 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/docker/docker v20.10.7+incompatible github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/go-units v0.4.0 + github.com/docker/swarmkit v1.12.1-0.20210211175721-17d8d4e4d8bd // indirect github.com/fvbommel/sortorder v1.0.2 // indirect github.com/go-git/go-git/v5 v5.4.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect diff --git a/go.sum b/go.sum index 29a9aee8d..2295955b3 100644 --- a/go.sum +++ b/go.sum @@ -217,6 +217,7 @@ github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/ github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= @@ -268,6 +269,7 @@ github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= @@ -278,6 +280,8 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNE github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/swarmkit v1.12.1-0.20210211175721-17d8d4e4d8bd h1:dXzP1rNMBwmZpvCH+FsQ9lEG1mv2+uJFxGo05+Eq78k= +github.com/docker/swarmkit v1.12.1-0.20210211175721-17d8d4e4d8bd/go.mod h1:n3Z4lIEl7g261ptkGDBcYi/3qBMDl9csaAhwi2MPejs= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=