refactor!: abra server interface more coherent

This follows our app new UX and interactive mode design.
This commit is contained in:
2021-10-22 10:31:33 +02:00
parent 94c7f59113
commit 313e3beb1e
7 changed files with 486 additions and 187 deletions

View File

@ -1,207 +1,148 @@
package server
import (
"context"
"errors"
"fmt"
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/libcapsul"
"github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var hetznerCloudType string
var hetznerCloudImage string
var hetznerCloudSSHKeys cli.StringSlice
var hetznerCloudLocation string
var hetznerCloudAPIToken string
var serverNewHetznerCloudCommand = &cli.Command{
Name: "hetzner",
Usage: "Create a new Hetzner virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Hetzner virtual server.
func newHetznerCloudVPS(c *cli.Context) error {
if err := internal.EnsureNewHetznerCloudVPSFlags(c); err != nil {
return err
}
This command uses the uses the Hetzner Cloud API bindings to send a server
creation request. You must already have a Hetzner Cloud account and an account
API token before using this command.
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
Your token can be loaded from the environment using the HCLOUD_TOKEN
environment variable or otherwise passing the "--token/-T" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Destination: &hetznerCloudType,
Value: "cx11",
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian-10",
Destination: &hetznerCloudImage,
},
&cli.StringSliceFlag{
Name: "ssh-keys",
Aliases: []string{"s"},
Usage: "SSH keys",
Destination: &hetznerCloudSSHKeys,
},
&cli.StringFlag{
Name: "location",
Aliases: []string{"l"},
Usage: "Server location",
Value: "hel1",
Destination: &hetznerCloudLocation,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Hetzner Cloud API token",
EnvVars: []string{"HCLOUD_TOKEN"},
Destination: &hetznerCloudAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
if hetznerCloudAPIToken == "" {
logrus.Fatal("Hetzner Cloud API token is missing")
}
ctx := context.Background()
client := hcloud.NewClient(hcloud.WithToken(hetznerCloudAPIToken))
logrus.Debugf("successfully created hetzner cloud API client")
var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("ssh-keys") {
sshKey, _, err := client.SSHKey.GetByName(ctx, sshKey)
if err != nil {
logrus.Fatal(err)
}
sshKeys = append(sshKeys, sshKey)
}
serverOpts := hcloud.ServerCreateOpts{
Name: name,
ServerType: &hcloud.ServerType{Name: hetznerCloudType},
Image: &hcloud.Image{Name: hetznerCloudImage},
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: hetznerCloudLocation},
}
res, _, err := client.Server.Create(ctx, serverOpts)
var sshKeysRaw []string
var sshKeys []*hcloud.SSHKey
for _, sshKey := range c.StringSlice("ssh-keys") {
sshKey, _, err := client.SSHKey.GetByName(c.Context, sshKey)
if err != nil {
logrus.Fatal(err)
return err
}
sshKeys = append(sshKeys, sshKey)
sshKeysRaw = append(sshKeysRaw, sshKey.Name)
}
logrus.Debugf("new server '%s' created", name)
serverOpts := hcloud.ServerCreateOpts{
Name: internal.HetznerCloudName,
ServerType: &hcloud.ServerType{Name: internal.HetznerCloudType},
Image: &hcloud.Image{Name: internal.HetznerCloudImage},
SSHKeys: sshKeys,
Location: &hcloud.Location{Name: internal.HetznerCloudLocation},
}
tableColumns := []string{"Name", "IPv4", "Root Password"}
table := formatter.CreateTable(tableColumns)
tableColumns := []string{"name", "type", "image", "ssh-keys", "location"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
internal.HetznerCloudName,
internal.HetznerCloudType,
internal.HetznerCloudImage,
strings.Join(sshKeysRaw, "\n"),
internal.HetznerCloudLocation,
})
table.Render()
if len(sshKeys) > 0 {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), "N/A (using SSH keys)"})
} else {
table.Append([]string{name, res.Server.PublicNet.IPv4.IP.String(), res.RootPassword})
}
response := false
prompt := &survey.Confirm{
Message: "continue with capsul creation?",
}
table.Render()
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
return nil
},
if !response {
logrus.Fatal("exiting as requested")
}
res, _, err := client.Server.Create(c.Context, serverOpts)
if err != nil {
return err
}
tableColumns = []string{"name", "ipv4", "root password"}
table = formatter.CreateTable(tableColumns)
if len(sshKeys) > 0 {
table.Append([]string{
internal.HetznerCloudName,
res.Server.PublicNet.IPv4.IP.String(),
"N/A (using SSH keys)",
})
} else {
table.Append([]string{
internal.HetznerCloudName,
res.Server.PublicNet.IPv4.IP.String(),
res.RootPassword,
})
}
table.SetCaption(true, "hetzner cloud creation response")
table.Render()
return nil
}
var capsulInstance string
var capsulType string
var capsulImage string
var capsulSSHKey string
var capsulAPIToken string
var serverNewCapsulCommand = &cli.Command{
Name: "capsul",
Usage: "Create a new Capsul virtual server",
ArgsUsage: "<name>",
Description: `
Create a new Capsul virtual server.
func newCapsulVPS(c *cli.Context) error {
if err := internal.EnsureNewCapsulVPSFlags(c); err != nil {
return err
}
This command uses the uses the Capsul API bindings of your chosen instance to
send a server creation request. You must already have an account on your chosen
Capsul instance before using this command.
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", internal.CapsulInstanceURL)
Your token can be loaded from the environment using the CAPSUL_TOKEN
environment variable or otherwise passing the "--token/-T" flag.
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance",
Aliases: []string{"I"},
Usage: "Capsul instance",
Destination: &capsulInstance,
Value: "yolo.servers.coop",
},
&cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Usage: "Server type",
Value: "f1-xs",
Destination: &capsulType,
},
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Usage: "Image type",
Value: "debian10",
Destination: &capsulImage,
},
&cli.StringFlag{
Name: "ssh-key",
Aliases: []string{"s"},
Usage: "SSH key",
Value: "",
Destination: &capsulSSHKey,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"T"},
Usage: "Capsul instance API token",
EnvVars: []string{"CAPSUL_TOKEN"},
Destination: &capsulAPIToken,
},
},
Action: func(c *cli.Context) error {
capsulName := c.Args().First()
if capsulName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no name provided"))
}
var sshKeys []string
for _, sshKey := range c.StringSlice("capsul-ssh-keys") {
sshKeys = append(sshKeys, sshKey)
}
if capsulAPIToken == "" {
logrus.Fatal("Capsul API token is missing")
}
tableColumns := []string{"instance", "name", "type", "image", "ssh-keys"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{
internal.CapsulInstanceURL,
internal.CapsulName,
internal.CapsulType,
internal.CapsulImage,
strings.Join(sshKeys, "\n"),
})
table.Render()
capsulCreateURL := fmt.Sprintf("https://%s/api/capsul/create", capsulInstance)
response := false
prompt := &survey.Confirm{
Message: "continue with capsul creation?",
}
capsulClient := libcapsul.New(capsulCreateURL, capsulAPIToken)
resp, err := capsulClient.Create(capsulName, capsulType, capsulImage, capsulSSHKey)
if err != nil {
logrus.Fatal(err)
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
tableColumns := []string{"Name", "ID"}
table := formatter.CreateTable(tableColumns)
table.Append([]string{capsulName, resp.ID})
table.Render()
if !response {
logrus.Fatal("exiting as requested")
}
return nil
},
capsulClient := libcapsul.New(capsulCreateURL, internal.CapsulAPIToken)
resp, err := capsulClient.Create(
internal.CapsulName,
internal.CapsulType,
internal.CapsulImage,
sshKeys,
)
if err != nil {
return err
}
tableColumns = []string{"Name", "ID"}
table = formatter.CreateTable(tableColumns)
table.Append([]string{internal.CapsulName, resp.ID})
table.SetCaption(true, "capsul creation response")
table.Render()
return nil
}
var serverNewCommand = &cli.Command{
@ -209,12 +150,52 @@ var serverNewCommand = &cli.Command{
Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider",
Description: `
Use a provider plugin to create a new server which can then be used to house a
new Co-op Cloud installation.
This command creates a new server via a 3rd party provider.
The following providers are supported:
Capsul https://git.cyberia.club/Cyberia/capsul-flask
Hetzner Cloud https://docs.hetzner.com/cloud
You may invoke this command in "wizard" mode and be prompted for input:
abra record new
`,
ArgsUsage: "<provider>",
Subcommands: []*cli.Command{
serverNewHetznerCloudCommand,
serverNewCapsulCommand,
Flags: []cli.Flag{
internal.ServerProviderFlag,
// Capsul
internal.CapsulInstanceURLFlag,
internal.CapsulTypeFlag,
internal.CapsulImageFlag,
internal.CapsulSSHKeysFlag,
internal.CapsulAPITokenFlag,
// Hetzner
internal.HetznerCloudNameFlag,
internal.HetznerCloudTypeFlag,
internal.HetznerCloudImageFlag,
internal.HetznerCloudSSHKeysFlag,
internal.HetznerCloudLocationFlag,
internal.HetznerCloudAPITokenFlag,
},
Action: func(c *cli.Context) error {
if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err)
}
switch internal.ServerProvider {
case "capsul":
if err := newCapsulVPS(c); err != nil {
logrus.Fatal(err)
}
case "hetzner-cloud":
if err := newHetznerCloudVPS(c); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}