forked from toolshed/abra
refactor!: server add provisions/deploys traefik
This commit is contained in:
parent
b72fa28ddb
commit
8cd9f2700f
@ -3,13 +3,21 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
"coopcloud.tech/abra/pkg/server"
|
"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/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
@ -17,20 +25,237 @@ import (
|
|||||||
var local bool
|
var local bool
|
||||||
var localFlag = &cli.BoolFlag{
|
var localFlag = &cli.BoolFlag{
|
||||||
Name: "local",
|
Name: "local",
|
||||||
Aliases: []string{"L"},
|
Aliases: []string{"l"},
|
||||||
Value: false,
|
Value: false,
|
||||||
Usage: "Set up the local server",
|
Usage: "Use local server",
|
||||||
Destination: &local,
|
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{
|
var serverAddCommand = &cli.Command{
|
||||||
Name: "add",
|
Name: "add",
|
||||||
Usage: "Add a new server",
|
Usage: "Add a new server",
|
||||||
Description: `
|
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
|
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
|
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
|
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
|
system username to make an initial connection. You can use the <user> and
|
||||||
<port> arguments to adjust this.
|
<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:
|
Abra will construct the following SSH connection string then:
|
||||||
|
|
||||||
ssh://globemodem@varia.zone:12345
|
ssh://globemodem@varia.zone:12345
|
||||||
|
|
||||||
All communication between Abra and the server will use this SSH connection.
|
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"},
|
Aliases: []string{"a"},
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
localFlag,
|
localFlag,
|
||||||
|
provisionFlag,
|
||||||
|
traefikFlag,
|
||||||
},
|
},
|
||||||
ArgsUsage: "<domain> [<user>] [<port>]",
|
ArgsUsage: "<domain> [<user>] [<port>]",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
|
|
||||||
if c.Args().Len() == 2 && !local {
|
if c.Args().Len() == 2 && !local {
|
||||||
err := errors.New("missing arguments <domain> or '--local'")
|
err := errors.New("missing arguments <domain> or '--local'")
|
||||||
internal.ShowSubcommandHelpAndError(c, err)
|
internal.ShowSubcommandHelpAndError(c, err)
|
||||||
@ -65,87 +299,43 @@ All communication between Abra and the server will use this SSH connection.
|
|||||||
internal.ShowSubcommandHelpAndError(c, err)
|
internal.ShowSubcommandHelpAndError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
domainName := "default"
|
|
||||||
|
|
||||||
if local {
|
if local {
|
||||||
if err := server.CreateServerDir(domainName); err != nil {
|
if err := newLocalServer(c, "default"); err != nil {
|
||||||
logrus.Fatal(err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
domainName = internal.ValidateDomain(c)
|
domainName := internal.ValidateDomain(c)
|
||||||
|
|
||||||
var username string
|
if err := createServerDir(domainName); err != nil {
|
||||||
var port string
|
logrus.Fatal(err)
|
||||||
|
|
||||||
username = c.Args().Get(1)
|
|
||||||
if username == "" {
|
|
||||||
systemUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
username = systemUser.Username
|
|
||||||
}
|
}
|
||||||
|
|
||||||
port = c.Args().Get(2)
|
if err := newContext(c, domainName); err != nil {
|
||||||
if port == "" {
|
logrus.Fatal(err)
|
||||||
port = "22"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
store := client.NewDefaultDockerContextStore()
|
cl, err := newClient(c, domainName)
|
||||||
contexts, err := store.Store.List()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, context := range contexts {
|
if provision {
|
||||||
if context.Name == domainName {
|
if err := installDocker(c, cl, domainName); err != nil {
|
||||||
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 {
|
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
logrus.Fatal(err)
|
if err := initSwarm(c, cl, domainName); err != nil {
|
||||||
}
|
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)
|
|
||||||
}
|
}
|
||||||
logrus.Debug(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("remote connection to '%s' is definitely up", domainName)
|
if traefik {
|
||||||
|
if err := deployTraefik(c, cl, domainName); err != nil {
|
||||||
if err := server.CreateServerDir(domainName); err != nil {
|
logrus.Fatal(err)
|
||||||
logrus.Fatal(err)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("server at '%s' has been added", domainName)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
},
|
|
||||||
}
|
|
@ -10,15 +10,12 @@ var ServerCommand = &cli.Command{
|
|||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
Usage: "Manage servers via 3rd party providers",
|
Usage: "Manage servers via 3rd party providers",
|
||||||
Description: `
|
Description: `
|
||||||
Manage the lifecycle of a server.
|
These commands support creating and managing servers using 3rd party
|
||||||
|
integrations. Servers can be provisioned from scratch so that they are capable
|
||||||
These commands support creating new servers using 3rd party integrations,
|
of hosing Co-op Cloud apps.
|
||||||
initialising existing servers to support Co-op Cloud deployments and managing
|
|
||||||
the connections to those servers.
|
|
||||||
`,
|
`,
|
||||||
Subcommands: []*cli.Command{
|
Subcommands: []*cli.Command{
|
||||||
serverNewCommand,
|
serverNewCommand,
|
||||||
serverInitCommand,
|
|
||||||
serverAddCommand,
|
serverAddCommand,
|
||||||
serverListCommand,
|
serverListCommand,
|
||||||
serverRemoveCommand,
|
serverRemoveCommand,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user