forked from toolshed/abra
		
	
		
			
				
	
	
		
			512 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			512 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package server
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"os/user"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"coopcloud.tech/abra/cli/internal"
 | |
| 	"coopcloud.tech/abra/pkg/client"
 | |
| 	"coopcloud.tech/abra/pkg/config"
 | |
| 	contextPkg "coopcloud.tech/abra/pkg/context"
 | |
| 	"coopcloud.tech/abra/pkg/dns"
 | |
| 	"coopcloud.tech/abra/pkg/server"
 | |
| 	"coopcloud.tech/abra/pkg/ssh"
 | |
| 	"github.com/AlecAivazis/survey/v2"
 | |
| 	"github.com/docker/docker/api/types"
 | |
| 	"github.com/docker/docker/api/types/swarm"
 | |
| 	dockerClient "github.com/docker/docker/client"
 | |
| 	"github.com/sirupsen/logrus"
 | |
| 	"github.com/urfave/cli"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	dockerInstallMsg = `
 | |
| A docker installation cannot be found on %s. This is a required system
 | |
| dependency for running Co-op Cloud apps on your server. If you would like, Abra
 | |
| can attempt to install Docker for you using the upstream non-interactive
 | |
| installation script.
 | |
| 
 | |
| See the following documentation for more:
 | |
| 
 | |
|     https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script
 | |
| 
 | |
| N.B Docker doesn't recommend it for production environments but many use it for
 | |
| such purposes. Docker stable is now installed by default by this script. The
 | |
| source for this script can be seen here:
 | |
| 
 | |
|     https://github.com/docker/docker-install
 | |
| `
 | |
| )
 | |
| 
 | |
| var local bool
 | |
| var localFlag = &cli.BoolFlag{
 | |
| 	Name:        "local, l",
 | |
| 	Usage:       "Use local server",
 | |
| 	Destination: &local,
 | |
| }
 | |
| 
 | |
| var provision bool
 | |
| var provisionFlag = &cli.BoolFlag{
 | |
| 	Name:        "provision, p",
 | |
| 	Usage:       "Provision server so it can deploy apps",
 | |
| 	Destination: &provision,
 | |
| }
 | |
| 
 | |
| var sshAuth string
 | |
| var sshAuthFlag = &cli.StringFlag{
 | |
| 	Name:        "ssh-auth, s",
 | |
| 	Value:       "identity-file",
 | |
| 	Usage:       "Select SSH authentication method (identity-file, password)",
 | |
| 	Destination: &sshAuth,
 | |
| }
 | |
| 
 | |
| var askSudoPass bool
 | |
| var askSudoPassFlag = &cli.BoolFlag{
 | |
| 	Name:        "ask-sudo-pass, a",
 | |
| 	Usage:       "Ask for sudo password",
 | |
| 	Destination: &askSudoPass,
 | |
| }
 | |
| 
 | |
| func cleanUp(domainName string) {
 | |
| 	logrus.Warnf("cleaning up context for %s", domainName)
 | |
| 	if err := client.DeleteContext(domainName); err != nil {
 | |
| 		logrus.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	logrus.Warnf("cleaning up server directory for %s", domainName)
 | |
| 	if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, domainName)); err != nil {
 | |
| 		logrus.Fatal(err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func installDockerLocal(c *cli.Context) error {
 | |
| 	fmt.Println(fmt.Sprintf(dockerInstallMsg, "this local server"))
 | |
| 
 | |
| 	response := false
 | |
| 	prompt := &survey.Confirm{
 | |
| 		Message: fmt.Sprintf("attempt install docker on local server?"),
 | |
| 	}
 | |
| 	if err := survey.AskOne(prompt, &response); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if !response {
 | |
| 		logrus.Fatal("exiting as requested")
 | |
| 	}
 | |
| 
 | |
| 	for _, exe := range []string{"wget", "bash"} {
 | |
| 		exists, err := ensureLocalExecutable(exe)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if !exists {
 | |
| 			return fmt.Errorf("%s missing, please install it", exe)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	cmd := exec.Command("bash", "-c", "wget -O- https://get.docker.com | bash")
 | |
| 	if err := internal.RunCmd(cmd); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func newLocalServer(c *cli.Context, domainName string) error {
 | |
| 	if err := createServerDir(domainName); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	cl, err := newClient(c, domainName)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if provision {
 | |
| 		exists, err := ensureLocalExecutable("docker")
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if !exists {
 | |
| 			if err := installDockerLocal(c); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if err := initSwarmLocal(c, cl, domainName); err != nil {
 | |
| 			if !strings.Contains(err.Error(), "proxy already exists") {
 | |
| 				logrus.Fatal(err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	logrus.Info("local server has been added")
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func newContext(c *cli.Context, domainName, username, port string) error {
 | |
| 	store := contextPkg.NewDefaultDockerContextStore()
 | |
| 	contexts, err := store.Store.List()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	for _, context := range contexts {
 | |
| 		if context.Name == domainName {
 | |
| 			logrus.Debugf("context for %s already exists", domainName)
 | |
| 			return nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port)
 | |
| 
 | |
| 	if err := client.CreateContext(domainName, username, port); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func newClient(c *cli.Context, domainName string) (*dockerClient.Client, error) {
 | |
| 	cl, err := client.New(domainName)
 | |
| 	if err != nil {
 | |
| 		return &dockerClient.Client{}, err
 | |
| 	}
 | |
| 	return cl, nil
 | |
| }
 | |
| 
 | |
| func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, domainName string) error {
 | |
| 	exists, err := ensureRemoteExecutable("docker", sshCl)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if !exists {
 | |
| 		fmt.Println(fmt.Sprintf(dockerInstallMsg, domainName))
 | |
| 
 | |
| 		response := false
 | |
| 		prompt := &survey.Confirm{
 | |
| 			Message: fmt.Sprintf("attempt install docker on %s?", domainName),
 | |
| 		}
 | |
| 
 | |
| 		if err := survey.AskOne(prompt, &response); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		if !response {
 | |
| 			logrus.Fatal("exiting as requested")
 | |
| 		}
 | |
| 
 | |
| 		exes := []string{"wget", "bash"}
 | |
| 		if askSudoPass {
 | |
| 			exes = append(exes, "ssh-askpass")
 | |
| 		}
 | |
| 
 | |
| 		for _, exe := range exes {
 | |
| 			exists, err := ensureRemoteExecutable(exe, sshCl)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			if !exists {
 | |
| 				return fmt.Errorf("%s missing on remote, please install it", exe)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		var sudoPass string
 | |
| 		if askSudoPass {
 | |
| 			cmd := "wget -O- https://get.docker.com | bash"
 | |
| 
 | |
| 			prompt := &survey.Password{
 | |
| 				Message: "sudo password?",
 | |
| 			}
 | |
| 
 | |
| 			if err := survey.AskOne(prompt, &sudoPass); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 
 | |
| 			logrus.Debugf("running %s on %s now with sudo password", cmd, domainName)
 | |
| 
 | |
| 			if sudoPass == "" {
 | |
| 				return fmt.Errorf("missing sudo password but requested --ask-sudo-pass?")
 | |
| 			}
 | |
| 
 | |
| 			logrus.Warn("installing docker, this could take some time...")
 | |
| 
 | |
| 			if err := ssh.RunSudoCmd(cmd, sudoPass, sshCl); err != nil {
 | |
| 				fmt.Print(fmt.Sprintf(`
 | |
| Abra was unable to bootstrap Docker, see below for logs:
 | |
| 
 | |
| 
 | |
| %s
 | |
| 
 | |
| If nothing works, you can try running the Docker install script manually on your server:
 | |
| 
 | |
|     wget -O- https://get.docker.com | bash
 | |
| 
 | |
| `, string(err.Error())))
 | |
| 				logrus.Fatal("Process exited with status 1")
 | |
| 			}
 | |
| 
 | |
| 			logrus.Infof("docker is installed on %s", domainName)
 | |
| 
 | |
| 			remoteUser := sshCl.SSHClient.Conn.User()
 | |
| 			logrus.Infof("adding %s to docker group", remoteUser)
 | |
| 			permsCmd := fmt.Sprintf("sudo usermod -aG docker %s", remoteUser)
 | |
| 			if err := ssh.RunSudoCmd(permsCmd, sudoPass, sshCl); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		} else {
 | |
| 			cmd := "wget -O- https://get.docker.com | bash"
 | |
| 
 | |
| 			logrus.Debugf("running %s on %s now without sudo password", cmd, domainName)
 | |
| 
 | |
| 			logrus.Warn("installing docker, this could take some time...")
 | |
| 
 | |
| 			if out, err := sshCl.Exec(cmd); err != nil {
 | |
| 				fmt.Print(fmt.Sprintf(`
 | |
| Abra was unable to bootstrap Docker, see below for logs:
 | |
| 
 | |
| 
 | |
| %s
 | |
| 
 | |
| This could be due to several reasons. One of the most common is that your
 | |
| server user account does not have sudo access, and if it does, you need to pass
 | |
| "--ask-sudo-pass" in order to supply Abra with your password.
 | |
| 
 | |
| If nothing works, you try running the Docker install script manually on your server:
 | |
| 
 | |
|     wget -O- https://get.docker.com | bash
 | |
| 
 | |
| `, string(out)))
 | |
| 				logrus.Fatal(err)
 | |
| 			}
 | |
| 
 | |
| 			logrus.Infof("docker is installed on %s", domainName)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error {
 | |
| 	initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"}
 | |
| 	if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
 | |
| 		if strings.Contains(err.Error(), "is already part of a swarm") ||
 | |
| 			strings.Contains(err.Error(), "must specify a listening address") {
 | |
| 			logrus.Infof("swarm mode already initialised on %s", domainName)
 | |
| 		} else {
 | |
| 			return err
 | |
| 		}
 | |
| 	} else {
 | |
| 		logrus.Infof("initialised swarm mode on local server")
 | |
| 	}
 | |
| 
 | |
| 	netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
 | |
| 	if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
 | |
| 		if !strings.Contains(err.Error(), "proxy already exists") {
 | |
| 			return err
 | |
| 		}
 | |
| 		logrus.Info("swarm overlay network already created on local server")
 | |
| 	} else {
 | |
| 		logrus.Infof("swarm overlay network created on local server")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error {
 | |
| 	ipv4, err := dns.EnsureIPv4(domainName)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	initReq := swarm.InitRequest{
 | |
| 		ListenAddr:    "0.0.0.0:2377",
 | |
| 		AdvertiseAddr: ipv4,
 | |
| 	}
 | |
| 	if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
 | |
| 		if strings.Contains(err.Error(), "is already part of a swarm") ||
 | |
| 			strings.Contains(err.Error(), "must specify a listening address") {
 | |
| 			logrus.Infof("swarm mode already initialised on %s", domainName)
 | |
| 		} else {
 | |
| 			return err
 | |
| 		}
 | |
| 	} else {
 | |
| 		logrus.Infof("initialised swarm mode on %s", domainName)
 | |
| 	}
 | |
| 
 | |
| 	netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
 | |
| 	if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
 | |
| 		if !strings.Contains(err.Error(), "proxy already exists") {
 | |
| 			return err
 | |
| 		}
 | |
| 		logrus.Infof("swarm overlay network already created on %s", domainName)
 | |
| 	} else {
 | |
| 		logrus.Infof("swarm overlay network created on %s", domainName)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func createServerDir(domainName string) error {
 | |
| 	if err := server.CreateServerDir(domainName); err != nil {
 | |
| 		if !os.IsExist(err) {
 | |
| 			return err
 | |
| 		}
 | |
| 		logrus.Debugf("server dir for %s already created", domainName)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var serverAddCommand = cli.Command{
 | |
| 	Name:    "add",
 | |
| 	Aliases: []string{"a"},
 | |
| 	Usage:   "Add a server to your configuration",
 | |
| 	Description: `
 | |
| Add a new server to your configuration so that it can be managed by Abra. This
 | |
| command can also provision your server ("--provision/-p") with a Docker
 | |
| installation so that it is capable of hosting Co-op Cloud apps.
 | |
| 
 | |
| Abra will default to expecting that you have a running ssh-agent and are using
 | |
| SSH keys to connect to your new server. Abra will also read your SSH config
 | |
| (matching "Host" as <domain>). SSH connection details precedence follows as
 | |
| such: command-line > SSH config > guessed defaults.
 | |
| 
 | |
| If you have no SSH key configured for this host and are instead using password
 | |
| authentication, you may pass "--ssh-auth password" to have Abra ask you for the
 | |
| password. "--ask-sudo-pass" may be passed if you run your provisioning commands
 | |
| via sudo privilege escalation.
 | |
| 
 | |
| The <domain> argument must be a publicy accessible domain name which points to
 | |
| your server. You should have working SSH access to this server already, Abra
 | |
| will assume port 22 and will use your current system username to make an
 | |
| initial connection. You can use the <user> and <port> arguments to adjust this.
 | |
| 
 | |
| Example:
 | |
| 
 | |
|     abra server add varia.zone glodemodem 12345 -p
 | |
| 
 | |
| Abra will construct the following SSH connection and Docker context:
 | |
| 
 | |
|     ssh://globemodem@varia.zone:12345
 | |
| 
 | |
| All communication between Abra and the server will use this SSH connection.
 | |
| 
 | |
| If "--local" is passed, then Abra assumes that the current local server is
 | |
| intended as the target server. This is useful when you want to have your entire
 | |
| Co-op Cloud config located on the server itself, and not on your local
 | |
| developer machine.
 | |
| `,
 | |
| 	Flags: []cli.Flag{
 | |
| 		internal.DebugFlag,
 | |
| 		internal.NoInputFlag,
 | |
| 		localFlag,
 | |
| 		provisionFlag,
 | |
| 		sshAuthFlag,
 | |
| 		askSudoPassFlag,
 | |
| 	},
 | |
| 	Before:    internal.SubCommandBefore,
 | |
| 	ArgsUsage: "<domain> [<user>] [<port>]",
 | |
| 	Action: func(c *cli.Context) error {
 | |
| 		if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
 | |
| 			err := errors.New("cannot use <domain> and --local together")
 | |
| 			internal.ShowSubcommandHelpAndError(c, err)
 | |
| 		}
 | |
| 
 | |
| 		if sshAuth != "password" && sshAuth != "identity-file" {
 | |
| 			err := errors.New("--ssh-auth only accepts identity-file or password")
 | |
| 			internal.ShowSubcommandHelpAndError(c, err)
 | |
| 		}
 | |
| 
 | |
| 		domainName := internal.ValidateDomain(c)
 | |
| 
 | |
| 		if local {
 | |
| 			if err := newLocalServer(c, "default"); err != nil {
 | |
| 				logrus.Fatal(err)
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		username := c.Args().Get(1)
 | |
| 		if username == "" {
 | |
| 			systemUser, err := user.Current()
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			username = systemUser.Username
 | |
| 		}
 | |
| 
 | |
| 		port := c.Args().Get(2)
 | |
| 		if port == "" {
 | |
| 			port = "22"
 | |
| 		}
 | |
| 
 | |
| 		if err := createServerDir(domainName); err != nil {
 | |
| 			logrus.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		if err := newContext(c, domainName, username, port); err != nil {
 | |
| 			logrus.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		cl, err := newClient(c, domainName)
 | |
| 		if err != nil {
 | |
| 			cleanUp(domainName)
 | |
| 			logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
 | |
| 			logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
 | |
| 		}
 | |
| 
 | |
| 		if provision {
 | |
| 			logrus.Debugf("attempting to construct SSH client for %s", domainName)
 | |
| 			sshCl, err := ssh.New(domainName, sshAuth, username, port)
 | |
| 			if err != nil {
 | |
| 				cleanUp(domainName)
 | |
| 				logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
 | |
| 			}
 | |
| 			defer sshCl.Close()
 | |
| 			logrus.Debugf("successfully created SSH client for %s", domainName)
 | |
| 
 | |
| 			if err := installDocker(c, cl, sshCl, domainName); err != nil {
 | |
| 				logrus.Fatal(err)
 | |
| 			}
 | |
| 			if err := initSwarm(c, cl, domainName); err != nil {
 | |
| 				logrus.Fatal(err)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if _, err := cl.Info(context.Background()); err != nil {
 | |
| 			cleanUp(domainName)
 | |
| 			logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	},
 | |
| }
 | |
| 
 | |
| // ensureLocalExecutable ensures that an executable is present on the local machine
 | |
| func ensureLocalExecutable(exe string) (bool, error) {
 | |
| 	out, err := exec.Command("which", exe).Output()
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	return string(out) != "", nil
 | |
| }
 | |
| 
 | |
| // ensureRemoteExecutable ensures that an executable is present on a remote machine
 | |
| func ensureRemoteExecutable(exe string, sshCl *ssh.Client) (bool, error) {
 | |
| 	out, err := sshCl.Exec(fmt.Sprintf("which %s", exe))
 | |
| 	if err != nil && string(out) != "" {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	return string(out) != "", nil
 | |
| }
 |