func MustParseSyncs(syncs []string) (result []sync.Sync) {
	for _, value := range syncs {
		sync, err := sync.Parse(value)
		if err != nil {
			logrus.Fatalf("Error reading sync argument: %s", err)
		}
		result = append(result, sync)
	}
	return result
}
MustParseSyncs parses a sync string in the form "~/local/dir:/data"
package cmd
import (
	"context"
	"encoding/csv"
	"fmt"
	"os"
	"os/signal"
	"os/user"
	"path/filepath"
	"strings"
	"syscall"
	"time"
	"github.com/ernoaapa/eliot/pkg/cmd"
	ui "github.com/ernoaapa/eliot/pkg/cmd/ui"
	"github.com/ernoaapa/eliot/pkg/discovery"
	"github.com/ernoaapa/eliot/pkg/printers"
	"github.com/ernoaapa/eliot/pkg/sync"
	"github.com/ernoaapa/eliot/pkg/utils"
	"github.com/sirupsen/logrus"
	"github.com/ernoaapa/eliot/pkg/api"
	containers "github.com/ernoaapa/eliot/pkg/api/services/containers/v1"
	pods "github.com/ernoaapa/eliot/pkg/api/services/pods/v1"
	"github.com/ernoaapa/eliot/pkg/config"
	"github.com/ernoaapa/eliot/pkg/fs"
	"github.com/ernoaapa/eliot/pkg/runtime"
	"github.com/urfave/cli"
)
const (
	outputHuman = "human"
	outputYaml  = "yaml"
)
var (
	GlobalFlags = []cli.Flag{
		cli.BoolFlag{
			Name:  "debug",
			Usage: "enable debug output in logs",
		},
		cli.BoolFlag{
			Name:  "quiet",
			Usage: "Don't print any progress output",
		},
		cli.StringFlag{
			Name:  "output, o",
			Usage: fmt.Sprintf("Output format. One of: %s", []string{outputHuman, outputYaml}),
			Value: "human",
		},
	}
)
func GlobalBefore(context *cli.Context) error {
	debug := context.GlobalBool("debug")
	if debug {
		logrus.SetLevel(logrus.DebugLevel)
	}
	if cmd.IsPipingOut() || context.GlobalBool("quiet") || context.GlobalString("output") != outputHuman {
		ui.SetOutput(ui.NewHidden())
	} else if debug {
		ui.SetOutput(ui.NewDebug())
	} else {
		ui.SetOutput(ui.NewTerminal())
	}
	return nil
}
func GetClient(config *config.Provider) *api.Client {
	uiline := ui.NewLine()
	endpoints := config.GetEndpoints()
	switch len(endpoints) {
	case 0:
		uiline.Fatal("No node to connect. You must give node endpoint. E.g. --endpoint=192.168.1.2")
		return nil
	case 1:
		uiline.Loadingf("Connecting to %s (%s)", endpoints[0].Name, endpoints[0].URL)
		client := api.NewClient(config.GetNamespace(), endpoints[0])
		info, err := client.GetInfo()
		if err != nil {
			logrus.Debugf("Connection failure: %s", err)
			uiline.Fatalf("Failed connect to %s (%s)", endpoints[0].Name, endpoints[0].URL)
		}
		uiline.Donef("Connected to %s (%s)", info.Hostname, endpoints[0].URL)
		return client
	default:
		uiline.Fatalf("%d node found. You must give target node. E.g. --endpoint=192.168.1.2", len(endpoints))
		return nil
	}
}
func GetConfig(clicontext *cli.Context) *config.Config {
	configPath := clicontext.GlobalString("config")
	conf, err := config.GetConfig(expandTilde(configPath))
	if err != nil {
		ui.NewLine().Fatalf("Error while reading configuration file [%s]: %s", configPath, err)
	}
	return conf
}
func GetConfigProvider(clicontext *cli.Context) *config.Provider {
	provider := config.NewProvider(GetConfig(clicontext))
	if clicontext.GlobalIsSet("namespace") && clicontext.GlobalString("namespace") != "" {
		provider.OverrideNamespace(clicontext.GlobalString("namespace"))
	}
	if clicontext.GlobalIsSet("endpoint") && clicontext.GlobalString("endpoint") != "" {
		provider.OverrideEndpoints([]config.Endpoint{{
			Name: clicontext.GlobalString("endpoint"),
			URL:  clicontext.GlobalString("endpoint"),
		}})
	}
	if len(provider.GetEndpoints()) == 0 {
		uiline := ui.NewLine().Loading("Discover from network automatically...")
		node, err := discovery.Nodes(2 * time.Second)
		if err != nil {
			uiline.Errorf("Failed to auto-discover node in network: %s", err)
		} else {
			if len(node) == 0 {
				uiline.Warn("No node discovered from network")
			} else {
				uiline.Donef("Discovered %d node(s) from network", len(node))
			}
		}
		endpoints := []config.Endpoint{}
		for _, node := range node {
			if len(node.Addresses) > 0 {
				endpoints = append(endpoints, config.Endpoint{
					Name: node.Hostname,
					URL:  fmt.Sprintf("%s:%d", utils.GetFirst(node.Addresses, ""), node.GrpcPort),
				})
			}
		}
		provider.OverrideEndpoints(endpoints)
	}
	if clicontext.GlobalIsSet("node") && clicontext.GlobalString("node") != "" {
		nodeName := clicontext.GlobalString("node")
		endpoint, found := provider.GetEndpointByName(nodeName)
		if !found {
			ui.NewLine().Errorf("Failed to find node with name %s", nodeName)
		}
		provider.OverrideEndpoints([]config.Endpoint{endpoint})
	}
	return provider
}
func UpdateConfig(clicontext *cli.Context, updated *config.Config) error {
	configPath := expandTilde(clicontext.GlobalString("config"))
	return config.WriteConfig(configPath, updated)
}
func GetLabels(clicontext *cli.Context) map[string]string {
	if !clicontext.IsSet("labels") {
		return map[string]string{}
	}
	param := clicontext.String("labels")
	values := strings.Split(param, ",")
	labels := map[string]string{}
	for _, value := range values {
		pair := strings.Split(value, "=")
		if len(pair) == 2 {
			labels[pair[0]] = pair[1]
		} else {
			ui.NewLine().Fatalf("Invalid --labels parameter [%s]. It must be comma separated key=value list. E.g. '--labels foo=bar,one=two'", param)
		}
	}
	return labels
}
func GetRuntimeClient(clicontext *cli.Context, hostname string) runtime.Client {
	return runtime.NewContainerdClient(
		context.Background(),
		clicontext.GlobalDuration("timeout"),
		clicontext.GlobalString("containerd"),
		clicontext.String("containerd-snapshotter"),
		hostname,
	)
}
func GetPrinter(clicontext *cli.Context) printers.ResourcePrinter {
	switch output := clicontext.GlobalString("output"); output {
	case outputHuman:
		return printers.NewHumanReadablePrinter()
	case outputYaml:
		return printers.NewYamlPrinter()
	default:
		logrus.Fatalf("Unknown output format: %s", output)
		return nil
	}
}
func MustParseMounts(mounts []string) (result []*containers.Mount) {
	for _, str := range mounts {
		mount, err := parseMountFlag(str)
		if err != nil {
			ui.NewLine().Fatalf("Failed to parse --mount flag [%s]: %s", str, err)
		}
		result = append(result, mount)
	}
	return result
}
func parseMountFlag(m string) (*containers.Mount, error) {
	mount := &containers.Mount{}
	r := csv.NewReader(strings.NewReader(m))
	fields, err := r.Read()
	if err != nil {
		return mount, err
	}
	for _, field := range fields {
		v := strings.Split(field, "=")
		if len(v) != 2 {
			return mount, fmt.Errorf("invalid mount specification: expected key=val")
		}
		key := v[0]
		val := v[1]
		switch key {
		case "type":
			mount.Type = val
		case "source", "src":
			mount.Source = val
		case "destination", "dst":
			mount.Destination = val
		case "options":
			mount.Options = strings.Split(val, ":")
		default:
			return mount, fmt.Errorf("mount option %q not supported", key)
		}
	}
	return mount, nil
}
func MustParseBinds(binds []string) (result []*containers.Mount) {
	for _, flag := range binds {
		bind, err := ParseBindFlag(flag)
		if err != nil {
			ui.NewLine().Fatalf("Failed to parse --bind flag: %s", err)
		}
		result = append(result, bind)
	}
	return result
}
func MustParseBindFlag(b string) *containers.Mount {
	m, err := ParseBindFlag(b)
	if err != nil {
		panic("Invalid mount format: " + b + ". Error: " + err.Error())
	}
	return m
}
func ParseBindFlag(b string) (*containers.Mount, error) {
	parts := strings.Split(b, ":")
	if len(parts) < 2 {
		return nil, fmt.Errorf("Cannot parse bind, missing ':': %s", b)
	}
	if len(parts) > 3 {
		return nil, fmt.Errorf("Cannot parse bind, too many ':': %s", b)
	}
	src := parts[0]
	dest := parts[1]
	opts := []string{"rw", "rbind", "rprivate"}
	if len(parts) == 3 {
		opts = append(strings.Split(parts[2], ","), "rbind")
	}
	return &containers.Mount{
		Type:        "bind",
		Destination: dest,
		Source:      src,
		Options:     opts,
	}, nil
}