diff options
Diffstat (limited to 'cmd/podman')
-rw-r--r-- | cmd/podman/main.go | 1 | ||||
-rw-r--r-- | cmd/podman/root.go | 31 | ||||
-rw-r--r-- | cmd/podman/system/connection.go | 201 | ||||
-rw-r--r-- | cmd/podman/system/connection/add.go | 223 | ||||
-rw-r--r-- | cmd/podman/system/connection/default.go | 46 | ||||
-rw-r--r-- | cmd/podman/system/connection/list.go | 84 | ||||
-rw-r--r-- | cmd/podman/system/connection/remove.go | 49 | ||||
-rw-r--r-- | cmd/podman/system/connection/rename.go | 54 |
8 files changed, 494 insertions, 195 deletions
diff --git a/cmd/podman/main.go b/cmd/podman/main.go index 5f740a006..f46f74547 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -14,6 +14,7 @@ import ( _ "github.com/containers/libpod/v2/cmd/podman/pods" "github.com/containers/libpod/v2/cmd/podman/registry" _ "github.com/containers/libpod/v2/cmd/podman/system" + _ "github.com/containers/libpod/v2/cmd/podman/system/connection" _ "github.com/containers/libpod/v2/cmd/podman/volumes" "github.com/containers/libpod/v2/pkg/rootless" "github.com/containers/libpod/v2/pkg/terminal" diff --git a/cmd/podman/root.go b/cmd/podman/root.go index c6ced21c0..e9f1ff710 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -236,16 +236,12 @@ func loggingHook() { func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { cfg := opts.Config + uri, ident := resolveDestination() lFlags := cmd.Flags() - custom, _ := config.ReadCustomConfig() - defaultURI := custom.Engine.RemoteURI - if defaultURI == "" { - defaultURI = registry.DefaultAPIAddress() - } lFlags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)") - lFlags.StringVar(&opts.URI, "url", defaultURI, "URL to access Podman service (CONTAINER_HOST)") - lFlags.StringVar(&opts.Identity, "identity", custom.Engine.RemoteIdentity, "path to SSH identity file, (CONTAINER_SSHKEY)") + lFlags.StringVar(&opts.URI, "url", uri, "URL to access Podman service (CONTAINER_HOST)") + lFlags.StringVar(&opts.Identity, "identity", ident, "path to SSH identity file, (CONTAINER_SSHKEY)") pFlags := cmd.PersistentFlags() pFlags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") @@ -292,3 +288,24 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { pFlags.BoolVar(&useSyslog, "syslog", false, "Output logging information to syslog as well as the console (default false)") } } + +func resolveDestination() (string, string) { + if uri, found := os.LookupEnv("CONTAINER_HOST"); found { + var ident string + if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found { + ident = v + } + return uri, ident + } + + cfg, err := config.ReadCustomConfig() + if err != nil { + return registry.DefaultAPIAddress(), "" + } + + uri, ident, err := cfg.ActiveDestination() + if err != nil { + return registry.DefaultAPIAddress(), "" + } + return uri, ident +} diff --git a/cmd/podman/system/connection.go b/cmd/podman/system/connection.go index 9f26a0df6..b1c538803 100644 --- a/cmd/podman/system/connection.go +++ b/cmd/podman/system/connection.go @@ -1,209 +1,34 @@ package system import ( - "bytes" - "fmt" - "net" - "net/url" - "os" - "os/user" - "regexp" - - "github.com/containers/common/pkg/config" "github.com/containers/libpod/v2/cmd/podman/registry" - "github.com/containers/libpod/v2/libpod/define" + "github.com/containers/libpod/v2/cmd/podman/validate" "github.com/containers/libpod/v2/pkg/domain/entities" - "github.com/containers/libpod/v2/pkg/terminal" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" ) -const schemaPattern = "^[A-Za-z][A-Za-z0-9+.-]*:" - var ( - // Skip creating engines since this command will obtain connection information to engine + // Skip creating engines since this command will obtain connection information to said engines noOp = func(cmd *cobra.Command, args []string) error { return nil } - connectionCmd = &cobra.Command{ - Use: "connection [flags] DESTINATION", - Args: cobra.ExactArgs(1), - Long: `Store ssh destination information in podman configuration. - "destination" is of the form [user@]hostname or - an URI of the form ssh://[user@]hostname[:port] -`, - Short: "Record remote ssh destination", - PersistentPreRunE: noOp, - PersistentPostRunE: noOp, - TraverseChildren: false, - RunE: connection, - Example: `podman system connection server.fubar.com - podman system connection --identity ~/.ssh/dev_rsa ssh://root@server.fubar.com:2222 - podman system connection --identity ~/.ssh/dev_rsa --port 22 root@server.fubar.com`, - } - cOpts = struct { - Identity string - Port int - UDSPath string - }{} + ConnectionCmd = &cobra.Command{ + Use: "connection", + Short: "Manage remote ssh destinations", + Long: `Manage ssh destination information in podman configuration`, + DisableFlagsInUseLine: true, + PersistentPreRunE: noOp, + RunE: validate.SubCommandExists, + PersistentPostRunE: noOp, + TraverseChildren: false, + } ) func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, - Command: connectionCmd, + Command: ConnectionCmd, Parent: systemCmd, }) - - flags := connectionCmd.Flags() - flags.IntVarP(&cOpts.Port, "port", "p", 22, "SSH port number for destination") - flags.StringVar(&cOpts.Identity, "identity", "", "path to SSH identity file") - flags.StringVar(&cOpts.UDSPath, "socket-path", "", "path to podman socket on remote host. (default '/run/podman/podman.sock' or '/run/user/{uid}/podman/podman.sock)") -} - -func connection(cmd *cobra.Command, args []string) error { - // Default to ssh: schema if none given - dest := []byte(args[0]) - if match, err := regexp.Match(schemaPattern, dest); err != nil { - return errors.Wrapf(err, "internal regex error %q", schemaPattern) - } else if !match { - dest = append([]byte("ssh://"), dest...) - } - - uri, err := url.Parse(string(dest)) - if err != nil { - return errors.Wrapf(err, "failed to parse %q", string(dest)) - } - - if uri.User.Username() == "" { - if uri.User, err = getUserInfo(uri); err != nil { - return err - } - } - - if cmd.Flag("socket-path").Changed { - uri.Path = cmd.Flag("socket-path").Value.String() - } - - if cmd.Flag("port").Changed { - uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").Value.String()) - } - - if uri.Port() == "" { - uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").DefValue) - } - - if uri.Path == "" { - if uri.Path, err = getUDS(cmd, uri); err != nil { - return errors.Wrapf(err, "failed to connect to %q", uri.String()) - } - } - - custom, err := config.ReadCustomConfig() - if err != nil { - return err - } - - if cmd.Flag("identity").Changed { - custom.Engine.RemoteIdentity = cOpts.Identity - } - - custom.Engine.RemoteURI = uri.String() - return custom.Write() -} - -func getUserInfo(uri *url.URL) (*url.Userinfo, error) { - var ( - usr *user.User - err error - ) - if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found { - usr, err = user.LookupId(u) - if err != nil { - return nil, errors.Wrapf(err, "failed to find user %q", u) - } - } else { - usr, err = user.Current() - if err != nil { - return nil, errors.Wrapf(err, "failed to obtain current user") - } - } - - pw, set := uri.User.Password() - if set { - return url.UserPassword(usr.Username, pw), nil - } - return url.User(usr.Username), nil -} - -func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) { - var authMethods []ssh.AuthMethod - passwd, set := uri.User.Password() - if set { - authMethods = append(authMethods, ssh.Password(passwd)) - } - - ident := cmd.Flag("identity") - if ident.Changed { - auth, err := terminal.PublicKey(ident.Value.String(), []byte(passwd)) - if err != nil { - return "", errors.Wrapf(err, "Failed to read identity %q", ident.Value.String()) - } - authMethods = append(authMethods, auth) - } - - if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { - logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock) - - c, err := net.Dial("unix", sock) - if err != nil { - return "", err - } - a := agent.NewClient(c) - authMethods = append(authMethods, ssh.PublicKeysCallback(a.Signers)) - } - - config := &ssh.ClientConfig{ - User: uri.User.Username(), - Auth: authMethods, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - } - dial, err := ssh.Dial("tcp", uri.Host, config) - if err != nil { - return "", errors.Wrapf(err, "failed to connect to %q", uri.Host) - } - defer dial.Close() - - session, err := dial.NewSession() - if err != nil { - return "", errors.Wrapf(err, "failed to create new ssh session on %q", uri.Host) - } - defer session.Close() - - // Override podman binary for testing etc - podman := "podman" - if v, found := os.LookupEnv("PODMAN_BINARY"); found { - podman = v - } - run := podman + " info --format=json" - - var buffer bytes.Buffer - session.Stdout = &buffer - if err := session.Run(run); err != nil { - return "", errors.Wrapf(err, "failed to run %q", run) - } - - var info define.Info - if err := json.Unmarshal(buffer.Bytes(), &info); err != nil { - return "", errors.Wrapf(err, "failed to parse 'podman info' results") - } - - if info.Host.RemoteSocket == nil || len(info.Host.RemoteSocket.Path) == 0 { - return "", fmt.Errorf("remote podman %q failed to report its UDS socket", uri.Host) - } - return info.Host.RemoteSocket.Path, nil } diff --git a/cmd/podman/system/connection/add.go b/cmd/podman/system/connection/add.go new file mode 100644 index 000000000..7522eb190 --- /dev/null +++ b/cmd/podman/system/connection/add.go @@ -0,0 +1,223 @@ +package connection + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "net/url" + "os" + "os/user" + "regexp" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/v2/cmd/podman/registry" + "github.com/containers/libpod/v2/cmd/podman/system" + "github.com/containers/libpod/v2/libpod/define" + "github.com/containers/libpod/v2/pkg/domain/entities" + "github.com/containers/libpod/v2/pkg/terminal" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +const schemaPattern = "^[A-Za-z][A-Za-z0-9+.-]*:" + +var ( + addCmd = &cobra.Command{ + Use: "add [flags] NAME DESTINATION", + Args: cobra.ExactArgs(2), + Short: "Record destination for the Podman service", + Long: `Add destination to podman configuration. + "destination" is of the form [user@]hostname or + an URI of the form ssh://[user@]hostname[:port] +`, + RunE: add, + Example: `podman system connection add laptop server.fubar.com + podman system connection add --identity ~/.ssh/dev_rsa testing ssh://root@server.fubar.com:2222 + podman system connection add --identity ~/.ssh/dev_rsa --port 22 production root@server.fubar.com + `, + } + + cOpts = struct { + Identity string + Port int + UDSPath string + Default bool + }{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: addCmd, + Parent: system.ConnectionCmd, + }) + + flags := addCmd.Flags() + flags.IntVarP(&cOpts.Port, "port", "p", 22, "SSH port number for destination") + flags.StringVar(&cOpts.Identity, "identity", "", "path to SSH identity file") + flags.StringVar(&cOpts.UDSPath, "socket-path", "", "path to podman socket on remote host. (default '/run/podman/podman.sock' or '/run/user/{uid}/podman/podman.sock)") + flags.BoolVarP(&cOpts.Default, "default", "d", false, "Set connection to be default") +} + +func add(cmd *cobra.Command, args []string) error { + // Default to ssh: schema if none given + dest := args[1] + if match, err := regexp.Match(schemaPattern, []byte(dest)); err != nil { + return errors.Wrapf(err, "internal regex error %q", schemaPattern) + } else if !match { + dest = "ssh://" + dest + } + + uri, err := url.Parse(dest) + if err != nil { + return errors.Wrapf(err, "failed to parse %q", dest) + } + + if uri.User.Username() == "" { + if uri.User, err = getUserInfo(uri); err != nil { + return err + } + } + + if cmd.Flags().Changed("socket-path") { + uri.Path = cmd.Flag("socket-path").Value.String() + } + + if cmd.Flags().Changed("port") { + uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").Value.String()) + } + + if uri.Port() == "" { + uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").DefValue) + } + + if uri.Path == "" { + if uri.Path, err = getUDS(cmd, uri); err != nil { + return errors.Wrapf(err, "failed to connect to %q", uri.String()) + } + } + + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + + if cmd.Flags().Changed("default") { + if cOpts.Default { + cfg.Engine.ActiveService = args[0] + } + } + + dst := config.Destination{ + URI: uri.String(), + } + + if cmd.Flags().Changed("identity") { + dst.Identity = cOpts.Identity + } + + if cfg.Engine.ServiceDestinations == nil { + cfg.Engine.ServiceDestinations = map[string]config.Destination{ + args[0]: dst, + } + } else { + cfg.Engine.ServiceDestinations[args[0]] = dst + } + return cfg.Write() +} + +func getUserInfo(uri *url.URL) (*url.Userinfo, error) { + var ( + usr *user.User + err error + ) + if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found { + usr, err = user.LookupId(u) + if err != nil { + return nil, errors.Wrapf(err, "failed to find user %q", u) + } + } else { + usr, err = user.Current() + if err != nil { + return nil, errors.Wrapf(err, "failed to obtain current user") + } + } + + pw, set := uri.User.Password() + if set { + return url.UserPassword(usr.Username, pw), nil + } + return url.User(usr.Username), nil +} + +func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) { + var authMethods []ssh.AuthMethod + passwd, set := uri.User.Password() + if set { + authMethods = append(authMethods, ssh.Password(passwd)) + } + + if cmd.Flags().Changed("identity") { + value := cmd.Flag("identity").Value.String() + auth, err := terminal.PublicKey(value, []byte(passwd)) + if err != nil { + return "", errors.Wrapf(err, "Failed to read identity %q", value) + } + authMethods = append(authMethods, auth) + } + + if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { + logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock) + + c, err := net.Dial("unix", sock) + if err != nil { + return "", err + } + a := agent.NewClient(c) + authMethods = append(authMethods, ssh.PublicKeysCallback(a.Signers)) + } + + config := &ssh.ClientConfig{ + User: uri.User.Username(), + Auth: authMethods, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + dial, err := ssh.Dial("tcp", uri.Host, config) + if err != nil { + return "", errors.Wrapf(err, "failed to connect to %q", uri.Host) + } + defer dial.Close() + + session, err := dial.NewSession() + if err != nil { + return "", errors.Wrapf(err, "failed to create new ssh session on %q", uri.Host) + } + defer session.Close() + + // Override podman binary for testing etc + podman := "podman" + if v, found := os.LookupEnv("PODMAN_BINARY"); found { + podman = v + } + run := podman + " info --format=json" + + var buffer bytes.Buffer + session.Stdout = &buffer + if err := session.Run(run); err != nil { + return "", errors.Wrapf(err, "failed to run %q", run) + } + + var info define.Info + if err := json.Unmarshal(buffer.Bytes(), &info); err != nil { + return "", errors.Wrapf(err, "failed to parse 'podman info' results") + } + + if info.Host.RemoteSocket == nil || len(info.Host.RemoteSocket.Path) == 0 { + return "", fmt.Errorf("remote podman %q failed to report its UDS socket", uri.Host) + } + return info.Host.RemoteSocket.Path, nil +} diff --git a/cmd/podman/system/connection/default.go b/cmd/podman/system/connection/default.go new file mode 100644 index 000000000..b85343dc2 --- /dev/null +++ b/cmd/podman/system/connection/default.go @@ -0,0 +1,46 @@ +package connection + +import ( + "fmt" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/v2/cmd/podman/registry" + "github.com/containers/libpod/v2/cmd/podman/system" + "github.com/containers/libpod/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Skip creating engines since this command will obtain connection information to said engines + dfltCmd = &cobra.Command{ + Use: "default NAME", + Args: cobra.ExactArgs(1), + Short: "Set named destination as default", + Long: `Set named destination as default for the Podman service`, + DisableFlagsInUseLine: true, + RunE: defaultRunE, + Example: `podman system connection default testing`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: dfltCmd, + Parent: system.ConnectionCmd, + }) +} + +func defaultRunE(cmd *cobra.Command, args []string) error { + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + + if _, found := cfg.Engine.ServiceDestinations[args[0]]; !found { + return fmt.Errorf("%q destination is not defined. See \"podman system connection add ...\" to create a connection", args[0]) + } + + cfg.Engine.ActiveService = args[0] + return cfg.Write() +} diff --git a/cmd/podman/system/connection/list.go b/cmd/podman/system/connection/list.go new file mode 100644 index 000000000..c0a9087f5 --- /dev/null +++ b/cmd/podman/system/connection/list.go @@ -0,0 +1,84 @@ +package connection + +import ( + "os" + "text/tabwriter" + "text/template" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/v2/cmd/podman/registry" + "github.com/containers/libpod/v2/cmd/podman/system" + "github.com/containers/libpod/v2/cmd/podman/validate" + "github.com/containers/libpod/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + listCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Args: validate.NoArgs, + Short: "List destination for the Podman service(s)", + Long: `List destination information for the Podman service(s) in podman configuration`, + DisableFlagsInUseLine: true, + Example: `podman system connection list + podman system connection ls`, + RunE: list, + TraverseChildren: false, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: listCmd, + Parent: system.ConnectionCmd, + }) +} + +type namedDestination struct { + Name string + config.Destination +} + +func list(_ *cobra.Command, _ []string) error { + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + + if len(cfg.Engine.ServiceDestinations) == 0 { + return nil + } + + hdrs := []map[string]string{{ + "Identity": "Identity", + "Name": "Name", + "URI": "URI", + }} + + rows := make([]namedDestination, 0) + for k, v := range cfg.Engine.ServiceDestinations { + if k == cfg.Engine.ActiveService { + k += "*" + } + + r := namedDestination{ + Name: k, + Destination: config.Destination{ + Identity: v.Identity, + URI: v.URI, + }, + } + rows = append(rows, r) + } + + // TODO: Allow user to override format + format := "{{range . }}{{.Name}}\t{{.Identity}}\t{{.URI}}\n{{end}}" + tmpl := template.Must(template.New("connection").Parse(format)) + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + defer w.Flush() + + _ = tmpl.Execute(w, hdrs) + return tmpl.Execute(w, rows) +} diff --git a/cmd/podman/system/connection/remove.go b/cmd/podman/system/connection/remove.go new file mode 100644 index 000000000..a2ca66c8d --- /dev/null +++ b/cmd/podman/system/connection/remove.go @@ -0,0 +1,49 @@ +package connection + +import ( + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/v2/cmd/podman/registry" + "github.com/containers/libpod/v2/cmd/podman/system" + "github.com/containers/libpod/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Skip creating engines since this command will obtain connection information to said engines + rmCmd = &cobra.Command{ + Use: "remove NAME", + Args: cobra.ExactArgs(1), + Aliases: []string{"rm"}, + Long: `Delete named destination from podman configuration`, + Short: "Delete named destination", + DisableFlagsInUseLine: true, + RunE: rm, + Example: `podman system connection remove devl + podman system connection rm devl`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCmd, + Parent: system.ConnectionCmd, + }) +} + +func rm(_ *cobra.Command, args []string) error { + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + + if cfg.Engine.ServiceDestinations != nil { + delete(cfg.Engine.ServiceDestinations, args[0]) + } + + if cfg.Engine.ActiveService == args[0] { + cfg.Engine.ActiveService = "" + } + + return cfg.Write() +} diff --git a/cmd/podman/system/connection/rename.go b/cmd/podman/system/connection/rename.go new file mode 100644 index 000000000..d6cd55c31 --- /dev/null +++ b/cmd/podman/system/connection/rename.go @@ -0,0 +1,54 @@ +package connection + +import ( + "fmt" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/v2/cmd/podman/registry" + "github.com/containers/libpod/v2/cmd/podman/system" + "github.com/containers/libpod/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Skip creating engines since this command will obtain connection information to said engines + renameCmd = &cobra.Command{ + Use: "rename OLD NEW", + Aliases: []string{"mv"}, + Args: cobra.ExactArgs(2), + Short: "Rename \"old\" to \"new\"", + Long: `Rename destination for the Podman service from "old" to "new"`, + DisableFlagsInUseLine: true, + RunE: rename, + Example: `podman system connection rename laptop devl, + podman system connection mv laptop devl`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: renameCmd, + Parent: system.ConnectionCmd, + }) +} + +func rename(cmd *cobra.Command, args []string) error { + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + + if _, found := cfg.Engine.ServiceDestinations[args[0]]; !found { + return fmt.Errorf("%q destination is not defined. See \"podman system connection add ...\" to create a connection", args[0]) + } + + cfg.Engine.ServiceDestinations[args[1]] = cfg.Engine.ServiceDestinations[args[0]] + delete(cfg.Engine.ServiceDestinations, args[0]) + + if cfg.Engine.ActiveService == args[0] { + cfg.Engine.ActiveService = args[1] + } + + return cfg.Write() +} |