refactor!: server add provisions/deploys traefik

This commit is contained in:
decentral1se 2021-10-22 11:42:47 +02:00
parent b72fa28ddb
commit 8cd9f2700f
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
3 changed files with 259 additions and 153 deletions

View File

@ -3,13 +3,21 @@ package server
import (
"context"
"errors"
"fmt"
"net"
"os"
"os/exec"
"os/user"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/server"
"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"
)
@ -17,20 +25,237 @@ import (
var local bool
var localFlag = &cli.BoolFlag{
Name: "local",
Aliases: []string{"L"},
Aliases: []string{"l"},
Value: false,
Usage: "Set up the local server",
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 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
}
var serverAddCommand = &cli.Command{
Name: "add",
Usage: "Add a new server",
Description: `
This command adds a new server that abra will communicate with, to deploy apps.
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.
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server.
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.
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
@ -38,23 +263,32 @@ 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.
For example:
Example:
abra server add varia.zone glodemodem 12345
abra server add --provision --traefik varia.zone glodemodem 12345
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.
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,
traefikFlag,
},
ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error {
if c.Args().Len() == 2 && !local {
err := errors.New("missing arguments <domain> or '--local'")
internal.ShowSubcommandHelpAndError(c, err)
@ -65,87 +299,43 @@ All communication between Abra and the server will use this SSH connection.
internal.ShowSubcommandHelpAndError(c, err)
}
domainName := "default"
if local {
if err := server.CreateServerDir(domainName); err != nil {
if err := newLocalServer(c, "default"); err != nil {
logrus.Fatal(err)
}
if _, err := exec.LookPath("docker"); err != nil {
return errors.New("docker command not found on $PATH, is it installed?")
}
logrus.Info("local server has been added")
return nil
}
domainName = internal.ValidateDomain(c)
domainName := internal.ValidateDomain(c)
var username string
var port string
username = c.Args().Get(1)
if username == "" {
systemUser, err := user.Current()
if err != nil {
logrus.Fatal(err)
}
username = systemUser.Username
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
port = c.Args().Get(2)
if port == "" {
port = "22"
if err := newContext(c, domainName); err != nil {
logrus.Fatal(err)
}
store := client.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
cl, err := newClient(c, domainName)
if err != nil {
logrus.Fatal(err)
}
for _, context := range contexts {
if context.Name == domainName {
logrus.Fatalf("server at '%s' already exists?", domainName)
}
}
logrus.Debugf("creating context with domain '%s', username '%s' and port '%s'", domainName, username, port)
if err := client.CreateContext(domainName, username, port); err != nil {
logrus.Fatal(err)
}
ctx := context.Background()
cl, err := client.New(domainName)
if err != nil {
logrus.Warn("cleaning up context due to connection failure")
if err := client.DeleteContext(domainName); err != nil {
if provision {
if err := installDocker(c, cl, domainName); err != nil {
logrus.Fatal(err)
}
logrus.Fatal(err)
}
if _, err := cl.Info(ctx); err != nil {
if strings.Contains(err.Error(), "command not found") {
logrus.Fatalf("docker is not installed on '%s'?", domainName)
} else {
logrus.Warn("cleaning up context due to connection failure")
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Fatalf("unable to make a connection to '%s'?", domainName)
if err := initSwarm(c, cl, domainName); err != nil {
logrus.Fatal(err)
}
logrus.Debug(err)
}
logrus.Debugf("remote connection to '%s' is definitely up", domainName)
if err := server.CreateServerDir(domainName); err != nil {
logrus.Fatal(err)
if traefik {
if err := deployTraefik(c, cl, domainName); err != nil {
logrus.Fatal(err)
}
}
logrus.Infof("server at '%s' has been added", domainName)
return nil
},
}

View File

@ -1,81 +0,0 @@
package server
import (
"context"
"fmt"
"net"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverInitCommand = &cli.Command{
Name: "init",
Usage: "Initialise server for deploying apps",
Aliases: []string{"i"},
HideHelp: true,
ArgsUsage: "<domain>",
Description: `
Initialise swarm mode on the target <domain>.
This initialisation explicitly chooses the "single host swarm" mode which uses
the default IPv4 address as the advertising address. This can be re-configured
later for more advanced use cases.
`,
Action: func(c *cli.Context) error {
domainName := internal.ValidateDomain(c)
cl, err := client.New(domainName)
if err != nil {
return err
}
// 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),
}
// comrade librehosters DNS resolver https://snopyta.org/service/dns/
return d.DialContext(ctx, "udp", freifunkDNS)
},
}
logrus.Debugf("created DNS resolver via '%s'", freifunkDNS)
ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil {
logrus.Fatal(err)
}
if len(ips) == 0 {
return fmt.Errorf("unable to retrieve ipv4 address for %s", domainName)
}
ipv4 := ips[0].IP.To4().String()
initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4,
}
if _, err := cl.SwarmInit(ctx, initReq); err != nil {
return err
}
logrus.Debugf("initialised swarm on '%s'", domainName)
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
return err
}
logrus.Debug("swarm overlay network 'proxy' created")
return nil
},
}

View File

@ -10,15 +10,12 @@ var ServerCommand = &cli.Command{
Aliases: []string{"s"},
Usage: "Manage servers via 3rd party providers",
Description: `
Manage the lifecycle of a server.
These commands support creating new servers using 3rd party integrations,
initialising existing servers to support Co-op Cloud deployments and managing
the connections to those servers.
These commands support creating and managing servers using 3rd party
integrations. Servers can be provisioned from scratch so that they are capable
of hosing Co-op Cloud apps.
`,
Subcommands: []*cli.Command{
serverNewCommand,
serverInitCommand,
serverAddCommand,
serverListCommand,
serverRemoveCommand,