forked from toolshed/abra
		
	
		
			
				
	
	
		
			282 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			282 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package stack // https://github.com/docker/cli/blob/master/cli/command/stack/remove.go
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"os/signal"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"coopcloud.tech/abra/pkg/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"github.com/docker/docker/api/types"
 | |
| 	containerPkg "github.com/docker/docker/api/types/container"
 | |
| 	"github.com/docker/docker/api/types/network"
 | |
| 	"github.com/docker/docker/api/types/swarm"
 | |
| 	"github.com/docker/docker/api/types/versions"
 | |
| 	"github.com/docker/docker/client"
 | |
| 	apiclient "github.com/docker/docker/client"
 | |
| 	"github.com/pkg/errors"
 | |
| )
 | |
| 
 | |
| // RunRemove is the swarm implementation of docker stack remove
 | |
| func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
 | |
| 	log.Info(i18n.G("initialising undeploy"))
 | |
| 
 | |
| 	if WaitTimeout != 0 {
 | |
| 		log.Debug(i18n.G("timeout: set to %d second(s)", WaitTimeout))
 | |
| 	}
 | |
| 
 | |
| 	sigIntCh := make(chan os.Signal, 1)
 | |
| 	signal.Notify(sigIntCh, os.Interrupt)
 | |
| 	defer signal.Stop(sigIntCh)
 | |
| 
 | |
| 	waitCh := make(chan struct{})
 | |
| 	errCh := make(chan error)
 | |
| 
 | |
| 	go func() {
 | |
| 		var errs []string
 | |
| 		for _, namespace := range opts.Namespaces {
 | |
| 			services, err := GetStackServices(ctx, client, namespace)
 | |
| 			if err != nil {
 | |
| 				errCh <- err
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			networks, err := getStackNetworks(ctx, client, namespace)
 | |
| 			if err != nil {
 | |
| 				errCh <- err
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			var secrets []swarm.Secret
 | |
| 			if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
 | |
| 				secrets, err = getStackSecrets(ctx, client, namespace)
 | |
| 				if err != nil {
 | |
| 					errCh <- err
 | |
| 					return
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			var configs []swarm.Config
 | |
| 			if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
 | |
| 				configs, err = getStackConfigs(ctx, client, namespace)
 | |
| 				if err != nil {
 | |
| 					errCh <- err
 | |
| 					return
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
 | |
| 				log.Warn(i18n.G("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, i18n.G("failed to remove some resources from stack: %s", namespace))
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			log.Info(i18n.G("polling undeploy status"))
 | |
| 			timeout, err := waitOnTasks(ctx, client, namespace)
 | |
| 			if timeout {
 | |
| 				errs = append(errs, err.Error())
 | |
| 			} else {
 | |
| 				if err != nil {
 | |
| 					errs = append(errs, fmt.Sprint(i18n.G("failed to wait on tasks of stack: %s: %s", namespace, err)))
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if len(errs) > 0 {
 | |
| 			errCh <- errors.New(strings.Join(errs, "\n"))
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		close(waitCh)
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case <-waitCh:
 | |
| 		return nil
 | |
| 	case <-sigIntCh:
 | |
| 		return errors.New(i18n.G("skipping as requested, undeploy still in progress 🟠"))
 | |
| 	case err := <-errCh:
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	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 {
 | |
| 		log.Debug(i18n.G("removing service %s", service.Spec.Name))
 | |
| 		if err := client.ServiceRemove(ctx, service.ID); err != nil {
 | |
| 			hasError = true
 | |
| 			log.Fatal(i18n.G("failed to remove service %s: %s", service.ID, err))
 | |
| 		}
 | |
| 	}
 | |
| 	return hasError
 | |
| }
 | |
| 
 | |
| func removeNetworks(
 | |
| 	ctx context.Context,
 | |
| 	client *apiclient.Client,
 | |
| 	networks []network.Inspect,
 | |
| ) bool {
 | |
| 	var hasError bool
 | |
| 	for _, network := range networks {
 | |
| 		log.Debug(i18n.G("removing network %s", network.Name))
 | |
| 		if err := client.NetworkRemove(ctx, network.ID); err != nil {
 | |
| 			hasError = true
 | |
| 			log.Fatal(i18n.G("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 {
 | |
| 		log.Debug(i18n.G("removing secret %s", secret.Spec.Name))
 | |
| 		if err := client.SecretRemove(ctx, secret.ID); err != nil {
 | |
| 			hasError = true
 | |
| 			log.Fatal(i18n.G("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 {
 | |
| 		log.Debug(i18n.G("removing config %s", config.Spec.Name))
 | |
| 		if err := client.ConfigRemove(ctx, config.ID); err != nil {
 | |
| 			hasError = true
 | |
| 			log.Fatal(i18n.G("failed to remove config %s: %s", config.ID, err))
 | |
| 		}
 | |
| 	}
 | |
| 	return hasError
 | |
| }
 | |
| 
 | |
| // https://github.com/docker/cli/pull/4259
 | |
| func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) {
 | |
| 	return apiclient.TaskList(ctx, types.TaskListOptions{Filters: getStackFilter(namespace)})
 | |
| }
 | |
| 
 | |
| func getStackContainers(ctx context.Context, apiclient client.APIClient, namespace string) ([]containerPkg.Summary, error) {
 | |
| 	return apiclient.ContainerList(ctx, containerPkg.ListOptions{Filters: getStackFilter(namespace)})
 | |
| }
 | |
| 
 | |
| var numberedStates = map[swarm.TaskState]int64{
 | |
| 	swarm.TaskStateNew:       1,
 | |
| 	swarm.TaskStateAllocated: 2,
 | |
| 	swarm.TaskStatePending:   3,
 | |
| 	swarm.TaskStateAssigned:  4,
 | |
| 	swarm.TaskStateAccepted:  5,
 | |
| 	swarm.TaskStatePreparing: 6,
 | |
| 	swarm.TaskStateReady:     7,
 | |
| 	swarm.TaskStateStarting:  8,
 | |
| 	swarm.TaskStateRunning:   9,
 | |
| 	swarm.TaskStateComplete:  10,
 | |
| 	swarm.TaskStateShutdown:  11,
 | |
| 	swarm.TaskStateFailed:    12,
 | |
| 	swarm.TaskStateRejected:  13,
 | |
| }
 | |
| 
 | |
| func terminalState(state swarm.TaskState) bool {
 | |
| 	return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
 | |
| }
 | |
| 
 | |
| func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) {
 | |
| 	var timedOut bool
 | |
| 
 | |
| 	go func() {
 | |
| 		if WaitTimeout == 0 {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		log.Debug(i18n.G("timeout: waiting on undeploy tasks (timeout=%v secs)", WaitTimeout))
 | |
| 
 | |
| 		timeout := time.Duration(WaitTimeout) * time.Second
 | |
| 		<-time.After(timeout)
 | |
| 
 | |
| 		log.Debug(i18n.G("timed out on undeploy (timeout=%v sec)", WaitTimeout))
 | |
| 		timedOut = true
 | |
| 	}()
 | |
| 
 | |
| 	terminalStatesReached := 0
 | |
| 	for {
 | |
| 		tasks, err := getStackTasks(ctx, client, namespace)
 | |
| 		if err != nil {
 | |
| 			return false, errors.New(i18n.G("failed to get tasks: %w", err))
 | |
| 		}
 | |
| 
 | |
| 		for _, task := range tasks {
 | |
| 			if terminalState(task.Status.State) {
 | |
| 				log.Debug(i18n.G("waiting for %d task(s) to reach terminal state", len(tasks)-terminalStatesReached))
 | |
| 				terminalStatesReached++
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if terminalStatesReached == len(tasks) {
 | |
| 			log.Debug(i18n.G("all tasks reached terminal state"))
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		if timedOut {
 | |
| 			return true, errors.New(i18n.G("deployment timed out 🟠"))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	lastSeenCount := -1
 | |
| 	for {
 | |
| 		containers, err := getStackContainers(ctx, client, namespace)
 | |
| 		if err != nil {
 | |
| 			return false, errors.New(i18n.G("failed to list containers of stack: %s", namespace))
 | |
| 		}
 | |
| 
 | |
| 		numContainers := len(containers)
 | |
| 
 | |
| 		if numContainers == 0 {
 | |
| 			log.Debug(i18n.G("all containers did really go away"))
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		if numContainers != lastSeenCount {
 | |
| 			log.Debug(i18n.G("waiting for %d container(s) to really go away", numContainers))
 | |
| 			lastSeenCount = numContainers
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false, nil
 | |
| }
 |