2021-08-02 01:10:41 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2021-09-10 12:49:25 +00:00
|
|
|
"context"
|
2021-10-01 10:56:04 +00:00
|
|
|
"errors"
|
2021-10-22 09:42:47 +00:00
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"os"
|
2021-10-17 21:50:28 +00:00
|
|
|
"os/exec"
|
2021-09-10 12:49:25 +00:00
|
|
|
"os/user"
|
2021-09-22 06:19:28 +00:00
|
|
|
"strings"
|
2021-10-22 09:42:47 +00:00
|
|
|
"time"
|
2021-08-02 01:10:41 +00:00
|
|
|
|
2021-09-10 12:49:25 +00:00
|
|
|
"coopcloud.tech/abra/cli/internal"
|
2021-09-05 19:37:03 +00:00
|
|
|
"coopcloud.tech/abra/pkg/client"
|
2021-10-02 20:30:08 +00:00
|
|
|
"coopcloud.tech/abra/pkg/server"
|
2021-10-22 09:42:47 +00:00
|
|
|
"github.com/AlecAivazis/survey/v2"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
"github.com/docker/docker/api/types/swarm"
|
|
|
|
dockerClient "github.com/docker/docker/client"
|
2021-08-02 01:10:41 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
|
|
)
|
|
|
|
|
2021-10-01 09:59:17 +00:00
|
|
|
var local bool
|
|
|
|
var localFlag = &cli.BoolFlag{
|
|
|
|
Name: "local",
|
2021-10-22 09:42:47 +00:00
|
|
|
Aliases: []string{"l"},
|
2021-10-01 09:59:17 +00:00
|
|
|
Value: false,
|
2021-10-22 09:42:47 +00:00
|
|
|
Usage: "Use local server",
|
2021-10-01 09:59:17 +00:00
|
|
|
Destination: &local,
|
|
|
|
}
|
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
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 traefik bool
|
|
|
|
var traefikFlag = &cli.BoolFlag{
|
|
|
|
Name: "traefi",
|
|
|
|
Aliases: []string{"t"},
|
|
|
|
Value: false,
|
|
|
|
Usage: "Deploy traefik",
|
|
|
|
Destination: &traefik,
|
|
|
|
}
|
|
|
|
|
|
|
|
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 _, err := exec.LookPath("docker"); err != nil {
|
|
|
|
if provision {
|
|
|
|
if err := installDocker(c, cl, domainName); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := initSwarm(c, cl, domainName); err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logrus.Warn("no docker installation found, use '-p' to provision")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if traefik {
|
|
|
|
if err := deployTraefik(c, cl, domainName); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logrus.Warn("no traefik app found, use '-t' to deploy")
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Info("local server has been added")
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func newContext(c *cli.Context, domainName string) error {
|
|
|
|
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"
|
|
|
|
}
|
|
|
|
|
|
|
|
store := client.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 {
|
|
|
|
logrus.Warn("cleaning up context due to connection failure")
|
|
|
|
if err := client.DeleteContext(domainName); err != nil {
|
|
|
|
return &dockerClient.Client{}, err
|
|
|
|
}
|
|
|
|
return &dockerClient.Client{}, err
|
|
|
|
}
|
|
|
|
return cl, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func installDocker(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
|
|
|
if _, err := cl.Info(c.Context); err != nil {
|
|
|
|
if strings.Contains(err.Error(), "command not found") {
|
|
|
|
fmt.Println(fmt.Sprintf(`
|
|
|
|
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
|
|
|
|
|
|
|
|
`, 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")
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: implement this remote installer run
|
|
|
|
// https://stackoverflow.com/questions/37679939/how-do-i-execute-a-command-on-a-remote-machine-in-a-golang-cli
|
|
|
|
logrus.Warn("NOT IMPLEMENTED - COMING SOON")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error {
|
|
|
|
// comrade librehosters DNS resolver -> https://www.privacy-handbuch.de/handbuch_93d.htm
|
|
|
|
freifunkDNS := "5.1.66.255:53"
|
|
|
|
|
|
|
|
resolver := &net.Resolver{
|
|
|
|
PreferGo: false,
|
|
|
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
|
|
d := net.Dialer{
|
|
|
|
Timeout: time.Millisecond * time.Duration(10000),
|
|
|
|
}
|
|
|
|
return d.DialContext(ctx, "udp", freifunkDNS)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
|
|
|
|
|
|
|
|
ips, err := resolver.LookupIPAddr(c.Context, domainName)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(ips) == 0 {
|
|
|
|
return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
|
|
|
|
}
|
|
|
|
|
|
|
|
ipv4 := ips[0].IP.To4().String()
|
|
|
|
logrus.Debugf("discovered the following ipv4 addr: %s", ipv4)
|
|
|
|
|
|
|
|
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("initialised swarm mode on %s", domainName)
|
|
|
|
|
|
|
|
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
|
|
|
|
if _, err := cl.NetworkCreate(c.Context, "proxy", netOpts); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
// TODO: implement
|
|
|
|
logrus.Warn("NOT IMPLEMENTED - COMING SOON")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-08-02 01:10:41 +00:00
|
|
|
var serverAddCommand = &cli.Command{
|
2021-09-10 12:49:25 +00:00
|
|
|
Name: "add",
|
2021-10-22 11:31:14 +00:00
|
|
|
Usage: "Add a server to your Abra configuration",
|
2021-09-10 12:49:25 +00:00
|
|
|
Description: `
|
2021-10-22 09:42:47 +00:00
|
|
|
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. See below
|
|
|
|
for more on that.
|
2021-09-10 12:49:25 +00:00
|
|
|
|
2021-10-02 20:14:01 +00:00
|
|
|
If "--local" is passed, then Abra assumes that the current local server is
|
2021-10-22 09:42:47 +00:00
|
|
|
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.
|
2021-10-02 20:14:01 +00:00
|
|
|
|
|
|
|
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.
|
2021-09-10 12:49:25 +00:00
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
Example:
|
2021-09-10 12:49:25 +00:00
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
abra server add --provision --traefik varia.zone glodemodem 12345
|
2021-09-10 12:49:25 +00:00
|
|
|
|
|
|
|
Abra will construct the following SSH connection string then:
|
|
|
|
|
|
|
|
ssh://globemodem@varia.zone:12345
|
|
|
|
|
|
|
|
All communication between Abra and the server will use this SSH connection.
|
2021-10-22 09:42:47 +00:00
|
|
|
|
|
|
|
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.
|
2021-09-10 12:49:25 +00:00
|
|
|
`,
|
2021-10-01 10:56:04 +00:00
|
|
|
Aliases: []string{"a"},
|
2021-10-01 09:59:17 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
localFlag,
|
2021-10-22 09:42:47 +00:00
|
|
|
provisionFlag,
|
|
|
|
traefikFlag,
|
2021-10-01 09:59:17 +00:00
|
|
|
},
|
2021-09-10 12:49:25 +00:00
|
|
|
ArgsUsage: "<domain> [<user>] [<port>]",
|
2021-08-02 01:10:41 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2021-10-18 07:43:32 +00:00
|
|
|
if c.Args().Len() == 2 && !local {
|
2021-10-01 09:59:17 +00:00
|
|
|
err := errors.New("missing arguments <domain> or '--local'")
|
|
|
|
internal.ShowSubcommandHelpAndError(c, err)
|
|
|
|
}
|
|
|
|
|
2021-10-18 07:43:32 +00:00
|
|
|
if c.Args().Get(2) != "" && local {
|
2021-10-01 09:59:17 +00:00
|
|
|
err := errors.New("cannot use '<domain>' and '--local' together")
|
|
|
|
internal.ShowSubcommandHelpAndError(c, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if local {
|
2021-10-22 09:42:47 +00:00
|
|
|
if err := newLocalServer(c, "default"); err != nil {
|
2021-10-02 20:30:08 +00:00
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-10-01 09:59:17 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
domainName := internal.ValidateDomain(c)
|
2021-09-10 12:49:25 +00:00
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
if err := createServerDir(domainName); err != nil {
|
|
|
|
logrus.Fatal(err)
|
2021-09-10 12:49:25 +00:00
|
|
|
}
|
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
if err := newContext(c, domainName); err != nil {
|
|
|
|
logrus.Fatal(err)
|
2021-09-10 12:49:25 +00:00
|
|
|
}
|
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
cl, err := newClient(c, domainName)
|
2021-09-10 12:49:25 +00:00
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
if provision {
|
|
|
|
if err := installDocker(c, cl, domainName); err != nil {
|
|
|
|
logrus.Fatal(err)
|
2021-09-10 12:49:25 +00:00
|
|
|
}
|
2021-10-22 09:42:47 +00:00
|
|
|
if err := initSwarm(c, cl, domainName); err != nil {
|
2021-10-18 08:48:43 +00:00
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-08-02 01:10:41 +00:00
|
|
|
}
|
2021-09-10 12:49:25 +00:00
|
|
|
|
2021-10-22 09:42:47 +00:00
|
|
|
if traefik {
|
|
|
|
if err := deployTraefik(c, cl, domainName); err != nil {
|
|
|
|
logrus.Fatal(err)
|
2021-09-22 06:19:28 +00:00
|
|
|
}
|
2021-09-10 12:49:25 +00:00
|
|
|
}
|
|
|
|
|
2021-08-02 01:10:41 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|