decentral1se
85ff04202f
All checks were successful
continuous-integration/drone/push Build is passing
See coop-cloud/organising#227.
487 lines
13 KiB
Go
487 lines
13 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
|
"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/v2"
|
|
)
|
|
|
|
var (
|
|
dockerInstallMsg = `
|
|
A docker installation cannot be found on %s. This is a required system
|
|
dependency for running Co-op Cloud 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",
|
|
Aliases: []string{"l"},
|
|
Value: false,
|
|
Usage: "Use local server",
|
|
Destination: &local,
|
|
}
|
|
|
|
var provision bool
|
|
var provisionFlag = &cli.BoolFlag{
|
|
Name: "provision",
|
|
Aliases: []string{"p"},
|
|
Value: false,
|
|
Usage: "Provision server so it can deploy apps",
|
|
Destination: &provision,
|
|
}
|
|
|
|
var sshAuth string
|
|
var sshAuthFlag = &cli.StringFlag{
|
|
Name: "ssh-auth",
|
|
Aliases: []string{"sh"},
|
|
Value: "identity-file",
|
|
Usage: "Select SSH authentication method (identity-file, password)",
|
|
Destination: &sshAuth,
|
|
}
|
|
|
|
var askSudoPass bool
|
|
var askSudoPassFlag = &cli.BoolFlag{
|
|
Name: "ask-sudo-pass",
|
|
Aliases: []string{"as"},
|
|
Value: false,
|
|
Usage: "Ask for sudo password",
|
|
Destination: &askSudoPass,
|
|
}
|
|
|
|
var traefik bool
|
|
var traefikFlag = &cli.BoolFlag{
|
|
Name: "traefik",
|
|
Aliases: []string{"t"},
|
|
Value: false,
|
|
Usage: "Deploy traefik",
|
|
Destination: &traefik,
|
|
}
|
|
|
|
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.ABRA_SERVER_FOLDER, 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")
|
|
}
|
|
|
|
cmd := exec.Command("bash", "-c", "curl -s 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 {
|
|
out, err := exec.Command("which", "docker").Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if string(out) == "" {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if traefik {
|
|
if err := deployTraefik(c, cl, domainName); err != nil {
|
|
return 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 {
|
|
result, err := sshCl.Exec("which docker")
|
|
if err != nil && string(result) != "" {
|
|
return err
|
|
}
|
|
|
|
if string(result) == "" {
|
|
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")
|
|
}
|
|
|
|
cmd := "curl -s https://get.docker.com | bash"
|
|
|
|
var sudoPass string
|
|
if askSudoPass {
|
|
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 err := ssh.RunSudoCmd(cmd, sudoPass, sshCl); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
logrus.Debugf("running '%s' on %s now without sudo password", cmd, domainName)
|
|
if err := ssh.Exec(cmd, sshCl); err != nil {
|
|
return 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(c.Context, initReq); err != nil {
|
|
if !strings.Contains(err.Error(), "is already part of a swarm") {
|
|
return err
|
|
}
|
|
logrus.Info("swarm mode already initialised on local server")
|
|
} else {
|
|
logrus.Infof("initialised swarm mode on local server")
|
|
}
|
|
|
|
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
|
if _, err := cl.NetworkCreate(c.Context, "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(c.Context, initReq); err != nil {
|
|
if !strings.Contains(err.Error(), "is already part of a swarm") {
|
|
return err
|
|
}
|
|
logrus.Infof("swarm mode already initialised on %s", domainName)
|
|
} else {
|
|
logrus.Infof("initialised swarm mode on %s", domainName)
|
|
}
|
|
|
|
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
|
if _, err := cl.NetworkCreate(c.Context, "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
|
|
}
|
|
|
|
func deployTraefik(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
|
internal.NoInput = true
|
|
|
|
internal.RecipeName = "traefik"
|
|
internal.NewAppServer = domainName
|
|
internal.Domain = fmt.Sprintf("%s.%s", "traefik", domainName)
|
|
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 := internal.NewAction(c); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
} else {
|
|
logrus.Infof("%s already exists, not creating again", appEnvPath)
|
|
}
|
|
|
|
internal.AppName = internal.NewAppName
|
|
if err := internal.DeployAction(c); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var serverAddCommand = &cli.Command{
|
|
Name: "add",
|
|
Usage: "Add a server to your configuration",
|
|
Description: `
|
|
This command adds a new server to your configuration so that it can be managed
|
|
by Abra. This can be useful when you already have a server provisioned and want
|
|
to start running Abra commands against it.
|
|
|
|
This command can also provision your server ("--provision/-p") 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.
|
|
|
|
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.
|
|
|
|
Example:
|
|
|
|
abra server add --local
|
|
|
|
Otherwise, you may specify a remote server. The <domain> argument must be a
|
|
publicy accessible domain name which points to your server. You should have SSH
|
|
access to this server, 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 --provision --traefik varia.zone glodemodem 12345
|
|
|
|
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.
|
|
|
|
In this example, Abra will run the following operations:
|
|
|
|
1. Install Docker
|
|
2. Initialise Swarm mode
|
|
3. Deploy Traefik (core web proxy)
|
|
|
|
You may omit flags to avoid performing this provisioning logic.
|
|
`,
|
|
Aliases: []string{"a"},
|
|
Flags: []cli.Flag{
|
|
localFlag,
|
|
provisionFlag,
|
|
sshAuthFlag,
|
|
askSudoPassFlag,
|
|
traefikFlag,
|
|
},
|
|
ArgsUsage: "<domain> [<user>] [<port>]",
|
|
Action: func(c *cli.Context) error {
|
|
if c.Args().Len() > 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)
|
|
}
|
|
|
|
if local {
|
|
if err := newLocalServer(c, "default"); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
domainName, err := internal.ValidateDomain(c)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
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 {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
if provision {
|
|
logrus.Debugf("attempting to construct SSH client for %s", domainName)
|
|
sshCl, err := ssh.New(domainName, sshAuth, username, port)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
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(c.Context); err != nil {
|
|
cleanUp(domainName)
|
|
logrus.Fatalf("couldn't make a remote docker connection to %s? use --provision/-p to attempt to install", domainName)
|
|
}
|
|
|
|
if traefik {
|
|
if err := deployTraefik(c, cl, domainName); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|