From a07e71f7dfd94a926b7bf8173419092b423ba791 Mon Sep 17 00:00:00 2001 From: cellarspoon Date: Wed, 22 Dec 2021 20:08:15 +0100 Subject: [PATCH] fix: grand ssh, provisioning, perms refactor See https://git.coopcloud.tech/coop-cloud/organising/issues/280. See https://git.coopcloud.tech/coop-cloud/organising/issues/273. --- cli/autocomplete.go | 4 +- cli/catalogue/catalogue.go | 4 +- cli/cli.go | 2 +- cli/internal/new.go | 6 +-- cli/recipe/new.go | 2 +- cli/server/add.go | 102 +++++++++++++++++++++++++++---------- cli/server/remove.go | 2 +- pkg/catalogue/catalogue.go | 2 +- pkg/compose/compose.go | 4 +- pkg/config/app.go | 8 +-- pkg/config/env.go | 2 +- pkg/server/server.go | 2 +- pkg/ssh/ssh.go | 84 ++++-------------------------- 13 files changed, 106 insertions(+), 118 deletions(-) diff --git a/cli/autocomplete.go b/cli/autocomplete.go index 4739530bf..f064cd574 100644 --- a/cli/autocomplete.go +++ b/cli/autocomplete.go @@ -57,11 +57,11 @@ Supported shells are as follows: } autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion") - if err := os.Mkdir(autocompletionDir, 0644); err != nil { + if err := os.Mkdir(autocompletionDir, 0764); err != nil { if !os.IsExist(err) { logrus.Fatal(err) } - logrus.Debugf("'%s' already created, moving on...", autocompletionDir) + logrus.Debugf("%s already created", autocompletionDir) } autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType) diff --git a/cli/catalogue/catalogue.go b/cli/catalogue/catalogue.go index 43de4d8b0..c2c357546 100644 --- a/cli/catalogue/catalogue.go +++ b/cli/catalogue/catalogue.go @@ -201,7 +201,7 @@ A new catalogue copy can be published to the recipes repository by passing the } if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) { - if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil { + if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0764); err != nil { logrus.Fatal(err) } } else { @@ -216,7 +216,7 @@ A new catalogue copy can be published to the recipes repository by passing the if err != nil { logrus.Fatal(err) } - if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0644); err != nil { + if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0764); err != nil { logrus.Fatal(err) } } diff --git a/cli/cli.go b/cli/cli.go index 7f98454c8..a020d6d21 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -76,7 +76,7 @@ convenient command-line experience. See "abra autocomplete -h" for more. } for _, path := range paths { - if err := os.Mkdir(path, 0644); err != nil { + if err := os.Mkdir(path, 0764); err != nil { if !os.IsExist(err) { logrus.Fatal(err) } diff --git a/cli/internal/new.go b/cli/internal/new.go index ccda3bc27..8d63f1eda 100644 --- a/cli/internal/new.go +++ b/cli/internal/new.go @@ -129,11 +129,11 @@ func NewAction(c *cli.Context) error { sanitisedAppName := config.SanitiseAppName(NewAppName) if len(sanitisedAppName) > 45 { - logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName) + logrus.Fatalf("%s cannot be longer than 45 characters", sanitisedAppName) } - logrus.Debugf("'%s' sanitised as '%s' for new app", NewAppName, sanitisedAppName) + logrus.Debugf("%s sanitised as %s for new app", NewAppName, sanitisedAppName) - if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain, recipe.Name); err != nil { + if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain); err != nil { logrus.Fatal(err) } diff --git a/cli/recipe/new.go b/cli/recipe/new.go index 0cd69c9ce..162e722fd 100644 --- a/cli/recipe/new.go +++ b/cli/recipe/new.go @@ -61,7 +61,7 @@ The new example repository is cloned to ~/.abra/apps/. path.Join(config.APPS_DIR, recipeName, ".drone.yml"), } for _, path := range toParse { - file, err := os.OpenFile(path, os.O_RDWR, 0644) + file, err := os.OpenFile(path, os.O_RDWR, 0664) if err != nil { logrus.Fatal(err) } diff --git a/cli/server/add.go b/cli/server/add.go index 73f94e92d..789555492 100644 --- a/cli/server/add.go +++ b/cli/server/add.go @@ -10,7 +10,6 @@ import ( "path/filepath" "strings" - abraFormatter "coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" @@ -127,7 +126,7 @@ func installDockerLocal(c *cli.Context) error { } } - cmd := exec.Command("bash", "-c", "wget -O- https://install.abra.coopcloud.tech | bash") + cmd := exec.Command("bash", "-c", "wget -O- https://get.docker.com | bash") if err := internal.RunCmd(cmd); err != nil { return err } @@ -219,14 +218,21 @@ func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, d 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") } - for _, exe := range []string{"wget", "bash"} { + 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 @@ -236,40 +242,91 @@ func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, d } } - cmd := "wget -O- https://install.abra.coopcloud.tech | bash" - 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 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) - if err := ssh.Exec(cmd, sshCl); err != nil { - return err + + 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 a number of things but 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) } } - logrus.Infof("docker is already 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(c.Context, initReq); err != nil { - if !strings.Contains(err.Error(), "is already part of a swarm") { + 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 } - logrus.Info("swarm mode already initialised on local server") } else { logrus.Infof("initialised swarm mode on local server") } @@ -298,11 +355,12 @@ func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error AdvertiseAddr: ipv4, } if _, err := cl.SwarmInit(c.Context, initReq); err != nil { - if !strings.Contains(err.Error(), "is already part of a swarm") || - !strings.Contains(err.Error(), "must specify a listening address") { + 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 } - logrus.Infof("swarm mode already initialised on %s", domainName) } else { logrus.Infof("initialised swarm mode on %s", domainName) } @@ -339,16 +397,8 @@ func deployTraefik(c *cli.Context, cl *dockerClient.Client, domainName string) e internal.NewAppName = fmt.Sprintf("%s_%s", "traefik", config.SanitiseAppName(domainName)) appEnvPath := path.Join(config.ABRA_DIR, "servers", internal.Domain, fmt.Sprintf("%s.env", internal.NewAppName)) - if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) { - fmt.Println(fmt.Sprintf(` - You specified "--traefik/-t" and that means that Abra will now try to - automatically create a new Traefik app on %s. - `, internal.NewAppServer)) - - tableCol := []string{"recipe", "domain", "server", "name"} - table := abraFormatter.CreateTable(tableCol) - table.Append([]string{internal.RecipeName, internal.Domain, internal.NewAppServer, internal.NewAppName}) - + if _, err := os.Stat(appEnvPath); os.IsNotExist(err) { + logrus.Info(fmt.Sprintf("-t/--traefik specified, automatically deploying traefik to %s", internal.NewAppServer)) if err := internal.NewAction(c); err != nil { logrus.Fatal(err) } @@ -515,7 +565,7 @@ func ensureLocalExecutable(exe string) (bool, error) { return false, err } - return string(out) == "", nil + return string(out) != "", nil } // ensureRemoteExecutable ensures that an executable is present on a remote machine @@ -525,5 +575,5 @@ func ensureRemoteExecutable(exe string, sshCl *ssh.Client) (bool, error) { return false, err } - return string(out) == "", nil + return string(out) != "", nil } diff --git a/cli/server/remove.go b/cli/server/remove.go index 976e14ed9..fb4c97511 100644 --- a/cli/server/remove.go +++ b/cli/server/remove.go @@ -133,7 +133,7 @@ like tears in rain. response := false prompt := &survey.Confirm{ - Message: "are you sure there is no server to delete?", + Message: "prompt to actual server deletion?", } if err := survey.AskOne(prompt, &response); err != nil { logrus.Fatal(err) diff --git a/pkg/catalogue/catalogue.go b/pkg/catalogue/catalogue.go index bcf99aca5..f14adac03 100644 --- a/pkg/catalogue/catalogue.go +++ b/pkg/catalogue/catalogue.go @@ -209,7 +209,7 @@ func readRecipeCatalogueWeb(target interface{}) error { return err } - if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil { + if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0764); err != nil { return err } diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 27c8d278e..60f3e3252 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -71,7 +71,7 @@ func UpdateTag(pattern, image, tag, recipeName string) error { logrus.Debugf("updating '%s' to '%s' in '%s'", old, new, compose.Filename) - if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil { + if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { return err } } @@ -137,7 +137,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error { logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename) - if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil { + if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { return err } diff --git a/pkg/config/app.go b/pkg/config/app.go index 9c9b1f583..1fdf9e291 100644 --- a/pkg/config/app.go +++ b/pkg/config/app.go @@ -248,22 +248,22 @@ func GetAppNames() ([]string, error) { } // TemplateAppEnvSample copies the example env file for the app into the users env files -func TemplateAppEnvSample(appType, appName, server, domain, recipe string) error { - envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample") +func TemplateAppEnvSample(recipe, appName, server, domain string) error { + envSamplePath := path.Join(ABRA_DIR, "apps", recipe, ".env.sample") envSample, err := ioutil.ReadFile(envSamplePath) if err != nil { return err } appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName)) - if _, err := os.Stat(appEnvPath); err == nil { + if _, err := os.Stat(appEnvPath); os.IsExist(err) { return fmt.Errorf("%s already exists?", appEnvPath) } envSample = []byte(strings.Replace(string(envSample), fmt.Sprintf("%s.example.com", recipe), domain, -1)) envSample = []byte(strings.Replace(string(envSample), "example.com", domain, -1)) - err = ioutil.WriteFile(appEnvPath, envSample, 0644) + err = ioutil.WriteFile(appEnvPath, envSample, 0664) if err != nil { return err } diff --git a/pkg/config/env.go b/pkg/config/env.go index d3f66d561..b0085b96c 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -128,7 +128,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) { func EnsureAbraDirExists() error { if _, err := os.Stat(ABRA_DIR); os.IsNotExist(err) { logrus.Debugf("%s does not exist, creating it", ABRA_DIR) - if err := os.Mkdir(ABRA_DIR, 0777); err != nil { + if err := os.Mkdir(ABRA_DIR, 0764); err != nil { return err } } diff --git a/pkg/server/server.go b/pkg/server/server.go index 488df0f02..ec88a2710 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -12,7 +12,7 @@ import ( func CreateServerDir(serverName string) error { serverPath := path.Join(config.ABRA_DIR, "servers", serverName) - if err := os.Mkdir(serverPath, 0644); err != nil { + if err := os.Mkdir(serverPath, 0764); err != nil { if !os.IsExist(err) { return err } diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go index 0495b1527..9865390d7 100644 --- a/pkg/ssh/ssh.go +++ b/pkg/ssh/ssh.go @@ -111,7 +111,7 @@ type sudoWriter struct { // Write satisfies the write interface for sudoWriter func (w *sudoWriter) Write(p []byte) (int, error) { - if string(p) == "sudo_password" { + if strings.Contains(string(p), "sudo_password") { w.stdin.Write([]byte(w.pw + "\n")) w.pw = "" return len(p), nil @@ -131,11 +131,9 @@ func RunSudoCmd(cmd, passwd string, cl *Client) error { } defer session.Close() - cmd = "sudo -p " + "sudo_password" + " -S " + cmd + sudoCmd := fmt.Sprintf("SSH_ASKPASS=/usr/bin/ssh-askpass; sudo -p sudo_password -S %s", cmd) - w := &sudoWriter{ - pw: passwd, - } + w := &sudoWriter{pw: passwd} w.stdin, err = session.StdinPipe() if err != nil { return err @@ -144,79 +142,19 @@ func RunSudoCmd(cmd, passwd string, cl *Client) error { session.Stdout = w session.Stderr = w - done := make(chan struct{}) - scanner := bufio.NewScanner(session.Stdin) - - go func() { - for scanner.Scan() { - line := scanner.Text() - fmt.Println(line) - } - done <- struct{}{} - }() - - if err := session.Start(cmd); err != nil { - return err + modes := ssh.TerminalModes{ + ssh.ECHO: 0, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, } - <-done - - if err := session.Wait(); err != nil { - return err - } - - return err -} - -// Exec runs a command on a remote and streams output -func Exec(cmd string, cl *Client) error { - session, err := cl.SSHClient.NewSession() - if err != nil { - return err - } - defer session.Close() - - stdout, err := session.StdoutPipe() + err = session.RequestPty("xterm", 80, 40, modes) if err != nil { return err } - stderr, err := session.StdoutPipe() - if err != nil { - return err - } - - stdoutDone := make(chan struct{}) - stdoutScanner := bufio.NewScanner(stdout) - - go func() { - for stdoutScanner.Scan() { - line := stdoutScanner.Text() - fmt.Println(line) - } - stdoutDone <- struct{}{} - }() - - stderrDone := make(chan struct{}) - stderrScanner := bufio.NewScanner(stderr) - - go func() { - for stderrScanner.Scan() { - line := stderrScanner.Text() - fmt.Println(line) - } - stderrDone <- struct{}{} - }() - - if err := session.Start(cmd); err != nil { - return err - } - - <-stdoutDone - <-stderrDone - - if err := session.Wait(); err != nil { - return err + if err := session.Run(sudoCmd); err != nil { + return fmt.Errorf("%s", string(w.b.Bytes())) } return nil @@ -320,7 +258,7 @@ func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.Publ if exists { hostname := strings.Split(hostnameAndPort, ":")[0] - logrus.Debugf("server SSH host key found for %s, moving on", hostname) + logrus.Debugf("server SSH host key found for %s", hostname) return nil }