refactor: break up cli pkg into nice small chunks

This commit is contained in:
Roxie Gibson 2021-08-02 02:10:41 +01:00
parent c2f53e493e
commit 30d11f48a7
Signed by untrusted user: roxxers
GPG Key ID: 5D0140EDEE123F4D
29 changed files with 787 additions and 649 deletions

View File

@ -1,355 +0,0 @@
package cli
import (
"context"
"errors"
"fmt"
"path"
"sort"
"strings"
"coopcloud.tech/abra/catalogue"
"coopcloud.tech/abra/client"
"coopcloud.tech/abra/config"
"coopcloud.tech/abra/secret"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new app",
Description: `
This command takes an app recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <app>" to do so.
You can see what apps can be created (i.e. values for the <type> argument) by
running "abra recipe ls".
Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These
generated secrets are only visible at generation time, so please take care to
store them somewhere safe.
You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.
`,
Flags: []cli.Flag{
ServerFlag,
DomainFlag,
AppNameFlag,
PassFlag,
SecretsFlag,
},
ArgsUsage: "<type>",
Action: func(c *cli.Context) error {
appType := c.Args().First()
if appType == "" {
showSubcommandHelpAndError(c, errors.New("no app type provided"))
return nil
}
config.EnsureAbraDirExists()
appFiles, err := config.LoadAppFiles(Server)
if err != nil {
logrus.Fatal(err)
}
catl, err := catalogue.ReadAppsCatalogue()
if err != nil {
logrus.Fatal(err)
}
app := catl[appType]
app.EnsureExists()
latestVersion := app.LatestVersion()
if err := app.EnsureVersion(latestVersion); err != nil {
logrus.Fatal(err)
}
servers := appFiles.GetServers()
if Server == "" {
prompt := &survey.Select{
Message: "Select app server:",
Options: servers,
}
if err := survey.AskOne(prompt, &Server); err != nil {
logrus.Fatal(err)
}
}
if Domain == "" {
prompt := &survey.Input{
Message: "Specify app domain",
}
if err := survey.AskOne(prompt, &Domain); err != nil {
logrus.Fatal(err)
}
}
if AppName == "" {
prompt := &survey.Input{
Message: "Specify app name:",
Default: strings.ReplaceAll(Domain, ".", "_"),
}
if err := survey.AskOne(prompt, &AppName); err != nil {
logrus.Fatal(err)
}
}
sanitisedAppName := strings.ReplaceAll(AppName, ".", "_")
if len(sanitisedAppName) > 45 {
logrus.Fatal(fmt.Errorf("'%s' cannot be longer than 45 characters", sanitisedAppName))
}
if err := config.CopyAppEnvSample(appType, AppName, Server); err != nil {
logrus.Fatal(err)
}
secrets := make(map[string]string)
if Secrets {
appEnvPath := path.Join(config.ABRA_DIR, "servers", Server, fmt.Sprintf("%s.env", sanitisedAppName))
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
logrus.Fatal(err)
}
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
secrets, err = secret.GenerateSecrets(secretEnvVars, sanitisedAppName, Server)
if err != nil {
logrus.Fatal(err)
}
if Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, Server); err != nil {
logrus.Fatal(err)
}
}
}
}
tableCol := []string{"Name", "Domain", "Type", "Server"}
table := createTable(tableCol)
table.Append([]string{sanitisedAppName, Domain, appType, Server})
table.Render()
if Secrets {
secretCols := []string{"Name", "Value"}
secretTable := createTable(secretCols)
for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]})
}
secretTable.Render()
}
return nil
},
}
var appDeployCommand = &cli.Command{
Name: "deploy",
Flags: []cli.Flag{
UpdateFlag,
ForceFlag,
SkipVersionCheckFlag,
NoDomainPollFlag,
},
}
var appUndeployCommand = &cli.Command{
Name: "undeploy",
}
var appBackupCommand = &cli.Command{
Name: "backup",
Flags: []cli.Flag{AllFlag},
}
var appRestoreCommand = &cli.Command{
Name: "restore",
Flags: []cli.Flag{AllFlag},
ArgsUsage: "<service> [<backup file>]",
}
var appListCommand = &cli.Command{
Name: "list",
Usage: "List all managed apps",
Description: `
This command looks at your local file system listing of apps and servers (e.g.
in ~/.abra/) to generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.
`,
Aliases: []string{"ls"},
Flags: []cli.Flag{StatusFlag, ServerFlag, TypeFlag},
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(Server)
if err != nil {
logrus.Fatal(err)
}
apps, err := config.GetApps(appFiles)
if err != nil {
logrus.Fatal(err)
}
sort.Sort(config.ByServerAndType(apps))
statuses := map[string]string{}
tableCol := []string{"Server", "Type", "Domain"}
if Status {
tableCol = append(tableCol, "Status")
statuses, err = config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
}
table := createTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
for _, app := range apps {
var tableRow []string
if app.Type == Type || Type == "" {
// If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.File.Server, app.Type, app.Domain}
if Status {
if status, ok := statuses[app.StackName()]; ok {
tableRow = append(tableRow, status)
} else {
tableRow = append(tableRow, "unknown")
}
}
}
table.Append(tableRow)
}
table.Render()
return nil
},
}
var appCheckCommand = &cli.Command{
Name: "check",
}
var appCpCommand = &cli.Command{
Name: "cp",
ArgsUsage: "<src> <dst>",
}
var appConfigCommand = &cli.Command{
Name: "config",
}
var appLogsCommand = &cli.Command{
Name: "logs",
ArgsUsage: "[<service>]",
}
var appPsCommand = &cli.Command{
Name: "ps",
Action: func(c *cli.Context) error {
ctx := context.Background()
cl, err := client.NewClientWithContext(Context)
if err != nil {
logrus.Fatal(err)
}
tasks, err := cl.TaskList(ctx, types.TaskListOptions{})
if err != nil {
logrus.Fatal(err)
}
for _, task := range tasks {
resolver := idresolver.New(cl, false)
serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
if err != nil {
return err
}
fmt.Printf("%#v\n", serviceName)
}
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
logrus.Fatal(err)
}
table := createTable([]string{"ID", "Image", "Command", "Created", "Status", "Ports", "Names"})
var conTable [][]string
for _, container := range containers {
conRow := []string{
shortenID(container.ID),
removeSha(container.Image),
truncate(container.Command),
humanDuration(container.Created),
container.Status,
formatter.DisplayablePorts(container.Ports),
strings.Join(container.Names, ","),
}
conTable = append(conTable, conRow)
}
table.AppendBulk(conTable)
table.Render()
return nil
},
}
var appRemoveCommand = &cli.Command{
Name: "remove",
Flags: []cli.Flag{VolumesFlag, SecretsFlag},
}
var appRunCommand = &cli.Command{
Name: "run",
Flags: []cli.Flag{
NoTTYFlag,
UserFlag,
},
ArgsUsage: "<service> <args>...",
}
var appRollbackCommand = &cli.Command{
Name: "rollback",
ArgsUsage: "[<version>]",
}
// TODO: Replicating what the bash abra does might be hard
// with the mix of subcommands and flags
var appSecretCommand = &cli.Command{
Name: "secret",
Flags: []cli.Flag{AllFlag, PassFlag},
Action: func(c *cli.Context) error {
password, err := secret.GeneratePassphrases(1)
if err != nil {
logrus.Fatal(err)
}
fmt.Println(password)
return nil
},
}
var AppCommand = &cli.Command{
Name: "app",
Usage: "Manage your apps",
Description: `
This command provides all the functionality you need to manage the lifecycle of
your apps. From initial deployment to day-2 operations (e.g. backup/restore) to
scaling apps up and spinning them down.
`,
Subcommands: []*cli.Command{
appNewCommand,
appConfigCommand,
appDeployCommand,
appUndeployCommand,
appBackupCommand,
appRestoreCommand,
appRemoveCommand,
appCheckCommand,
appListCommand,
appPsCommand,
appLogsCommand,
appCpCommand,
appRunCommand,
appRollbackCommand,
appSecretCommand,
},
}

32
cli/app/app.go Normal file
View File

@ -0,0 +1,32 @@
package app
import (
"github.com/urfave/cli/v2"
)
var AppCommand = &cli.Command{
Name: "app",
Usage: "Manage your apps",
Description: `
This command provides all the functionality you need to manage the lifecycle of
your apps. From initial deployment to day-2 operations (e.g. backup/restore) to
scaling apps up and spinning them down.
`,
Subcommands: []*cli.Command{
appNewCommand,
appConfigCommand,
appDeployCommand,
appUndeployCommand,
appBackupCommand,
appRestoreCommand,
appRemoveCommand,
appCheckCommand,
appListCommand,
appPsCommand,
appLogsCommand,
appCpCommand,
appRunCommand,
appRollbackCommand,
appSecretCommand,
},
}

11
cli/app/backup.go Normal file
View File

@ -0,0 +1,11 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"github.com/urfave/cli/v2"
)
var appBackupCommand = &cli.Command{
Name: "backup",
Flags: []cli.Flag{internal.AllFlag},
}

7
cli/app/check.go Normal file
View File

@ -0,0 +1,7 @@
package app
import "github.com/urfave/cli/v2"
var appCheckCommand = &cli.Command{
Name: "check",
}

7
cli/app/config.go Normal file
View File

@ -0,0 +1,7 @@
package app
import "github.com/urfave/cli/v2"
var appConfigCommand = &cli.Command{
Name: "config",
}

8
cli/app/cp.go Normal file
View File

@ -0,0 +1,8 @@
package app
import "github.com/urfave/cli/v2"
var appCpCommand = &cli.Command{
Name: "cp",
ArgsUsage: "<src> <dst>",
}

16
cli/app/deploy.go Normal file
View File

@ -0,0 +1,16 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"github.com/urfave/cli/v2"
)
var appDeployCommand = &cli.Command{
Name: "deploy",
Flags: []cli.Flag{
internal.UpdateFlag,
internal.ForceFlag,
internal.SkipVersionCheckFlag,
internal.NoDomainPollFlag,
},
}

70
cli/app/list.go Normal file
View File

@ -0,0 +1,70 @@
package app
import (
"sort"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appListCommand = &cli.Command{
Name: "list",
Usage: "List all managed apps",
Description: `
This command looks at your local file system listing of apps and servers (e.g.
in ~/.abra/) to generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.
`,
Aliases: []string{"ls"},
Flags: []cli.Flag{internal.StatusFlag, internal.ServerFlag, internal.TypeFlag},
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(internal.Server)
if err != nil {
logrus.Fatal(err)
}
apps, err := config.GetApps(appFiles)
if err != nil {
logrus.Fatal(err)
}
sort.Sort(config.ByServerAndType(apps))
statuses := map[string]string{}
tableCol := []string{"Server", "Type", "Domain"}
if internal.Status {
tableCol = append(tableCol, "Status")
statuses, err = config.GetAppStatuses(appFiles)
if err != nil {
logrus.Fatal(err)
}
}
table := abraFormatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
for _, app := range apps {
var tableRow []string
if app.Type == internal.Type || internal.Type == "" {
// If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.File.Server, app.Type, app.Domain}
if internal.Status {
if status, ok := statuses[app.StackName()]; ok {
tableRow = append(tableRow, status)
} else {
tableRow = append(tableRow, "unknown")
}
}
}
table.Append(tableRow)
}
table.Render()
return nil
},
}

8
cli/app/logs.go Normal file
View File

@ -0,0 +1,8 @@
package app
import "github.com/urfave/cli/v2"
var appLogsCommand = &cli.Command{
Name: "logs",
ArgsUsage: "[<service>]",
}

153
cli/app/new.go Normal file
View File

@ -0,0 +1,153 @@
package app
import (
"errors"
"fmt"
"path"
"strings"
"coopcloud.tech/abra/catalogue"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/config"
"coopcloud.tech/abra/secret"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new app",
Description: `
This command takes an app recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <app>" to do so.
You can see what apps can be created (i.e. values for the <type> argument) by
running "abra recipe ls".
Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These
generated secrets are only visible at generation time, so please take care to
store them somewhere safe.
You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.
`,
Flags: []cli.Flag{
internal.ServerFlag,
internal.DomainFlag,
internal.AppNameFlag,
internal.PassFlag,
internal.SecretsFlag,
},
ArgsUsage: "<type>",
Action: func(c *cli.Context) error {
appType := c.Args().First()
if appType == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no app type provided"))
return nil
}
config.EnsureAbraDirExists()
appFiles, err := config.LoadAppFiles(internal.Server)
if err != nil {
logrus.Fatal(err)
}
catl, err := catalogue.ReadAppsCatalogue()
if err != nil {
logrus.Fatal(err)
}
app := catl[appType]
app.EnsureExists()
latestVersion := app.LatestVersion()
if err := app.EnsureVersion(latestVersion); err != nil {
logrus.Fatal(err)
}
servers := appFiles.GetServers()
if internal.Server == "" {
prompt := &survey.Select{
Message: "Select app server:",
Options: servers,
}
if err := survey.AskOne(prompt, &internal.Server); err != nil {
logrus.Fatal(err)
}
}
if internal.Domain == "" {
prompt := &survey.Input{
Message: "Specify app domain",
}
if err := survey.AskOne(prompt, &internal.Domain); err != nil {
logrus.Fatal(err)
}
}
if internal.AppName == "" {
prompt := &survey.Input{
Message: "Specify app name:",
Default: strings.ReplaceAll(internal.Domain, ".", "_"),
}
if err := survey.AskOne(prompt, &internal.AppName); err != nil {
logrus.Fatal(err)
}
}
sanitisedAppName := strings.ReplaceAll(internal.AppName, ".", "_")
if len(sanitisedAppName) > 45 {
logrus.Fatal(fmt.Errorf("'%s' cannot be longer than 45 characters", sanitisedAppName))
}
if err := config.CopyAppEnvSample(appType, internal.AppName, internal.Server); err != nil {
logrus.Fatal(err)
}
secrets := make(map[string]string)
if internal.Secrets {
appEnvPath := path.Join(config.ABRA_DIR, "servers", internal.Server, fmt.Sprintf("%s.env", sanitisedAppName))
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
logrus.Fatal(err)
}
secretEnvVars := secret.ReadSecretEnvVars(appEnv)
secrets, err = secret.GenerateSecrets(secretEnvVars, sanitisedAppName, internal.Server)
if err != nil {
logrus.Fatal(err)
}
if internal.Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, internal.Server); err != nil {
logrus.Fatal(err)
}
}
}
}
tableCol := []string{"Name", "Domain", "Type", "Server"}
table := abraFormatter.CreateTable(tableCol)
table.Append([]string{sanitisedAppName, internal.Domain, appType, internal.Server})
table.Render()
if internal.Secrets {
secretCols := []string{"Name", "Value"}
secretTable := abraFormatter.CreateTable(secretCols)
for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]})
}
secretTable.Render()
}
return nil
},
}

61
cli/app/ps.go Normal file
View File

@ -0,0 +1,61 @@
package app
import (
"context"
"fmt"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/client"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appPsCommand = &cli.Command{
Name: "ps",
Action: func(c *cli.Context) error {
ctx := context.Background()
cl, err := client.NewClientWithContext(internal.Context)
if err != nil {
logrus.Fatal(err)
}
tasks, err := cl.TaskList(ctx, types.TaskListOptions{})
if err != nil {
logrus.Fatal(err)
}
for _, task := range tasks {
resolver := idresolver.New(cl, false)
serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID)
if err != nil {
return err
}
fmt.Printf("%#v\n", serviceName)
}
containers, err := cl.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
logrus.Fatal(err)
}
table := abraFormatter.CreateTable([]string{"ID", "Image", "Command", "Created", "Status", "Ports", "Names"})
var conTable [][]string
for _, container := range containers {
conRow := []string{
abraFormatter.ShortenID(container.ID),
abraFormatter.RemoveSha(container.Image),
abraFormatter.Truncate(container.Command),
abraFormatter.HumanDuration(container.Created),
container.Status,
formatter.DisplayablePorts(container.Ports),
strings.Join(container.Names, ","),
}
conTable = append(conTable, conRow)
}
table.AppendBulk(conTable)
table.Render()
return nil
},
}

11
cli/app/remove.go Normal file
View File

@ -0,0 +1,11 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"github.com/urfave/cli/v2"
)
var appRemoveCommand = &cli.Command{
Name: "remove",
Flags: []cli.Flag{internal.VolumesFlag, internal.SecretsFlag},
}

12
cli/app/restore.go Normal file
View File

@ -0,0 +1,12 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"github.com/urfave/cli/v2"
)
var appRestoreCommand = &cli.Command{
Name: "restore",
Flags: []cli.Flag{internal.AllFlag},
ArgsUsage: "<service> [<backup file>]",
}

8
cli/app/rollback.go Normal file
View File

@ -0,0 +1,8 @@
package app
import "github.com/urfave/cli/v2"
var appRollbackCommand = &cli.Command{
Name: "rollback",
ArgsUsage: "[<version>]",
}

15
cli/app/run.go Normal file
View File

@ -0,0 +1,15 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"github.com/urfave/cli/v2"
)
var appRunCommand = &cli.Command{
Name: "run",
Flags: []cli.Flag{
internal.NoTTYFlag,
internal.UserFlag,
},
ArgsUsage: "<service> <args>...",
}

25
cli/app/secret.go Normal file
View File

@ -0,0 +1,25 @@
package app
import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/secret"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// TODO: Replicating what the bash abra does might be hard
// with the mix of subcommands and flags
var appSecretCommand = &cli.Command{
Name: "secret",
Flags: []cli.Flag{internal.AllFlag, internal.PassFlag},
Action: func(c *cli.Context) error {
password, err := secret.GeneratePassphrases(1)
if err != nil {
logrus.Fatal(err)
}
fmt.Println(password)
return nil
},
}

7
cli/app/undeploy.go Normal file
View File

@ -0,0 +1,7 @@
package app
import "github.com/urfave/cli/v2"
var appUndeployCommand = &cli.Command{
Name: "undeploy",
}

View File

@ -4,6 +4,10 @@ import (
"fmt"
"os"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
@ -22,21 +26,21 @@ func RunApp(version, commit string) {
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{
AppCommand,
ServerCommand,
RecipeCommand,
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
VersionCommand,
},
Flags: []cli.Flag{
EnvFlag,
StackFlag,
SkipCheckFlag,
SkipUpdateFlag,
VerboseFlag,
BranchFlag,
NoPromptFlag,
DebugFlag,
ContextFlag,
internal.EnvFlag,
internal.StackFlag,
internal.SkipCheckFlag,
internal.SkipUpdateFlag,
internal.VerboseFlag,
internal.BranchFlag,
internal.NoPromptFlag,
internal.DebugFlag,
internal.ContextFlag,
},
}

View File

@ -1,4 +1,4 @@
package cli
package formatter
import (
"fmt"
@ -11,27 +11,27 @@ import (
"github.com/olekukonko/tablewriter"
)
func shortenID(str string) string {
func ShortenID(str string) string {
return str[:12]
}
func truncate(str string) string {
func Truncate(str string) string {
return fmt.Sprintf(`"%s"`, formatter.Ellipsis(str, 19))
}
// removeSha remove image sha from a string that are added in some docker outputs
func removeSha(str string) string {
// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
// humanDuration from docker/cli RunningFor() to be accessable outside of the class
func humanDuration(timestamp int64) string {
// HumanDuration from docker/cli RunningFor() to be accessable outside of the class
func HumanDuration(timestamp int64) string {
date := time.Unix(timestamp, 0)
now := time.Now().UTC()
return units.HumanDuration(now.Sub(date)) + " ago"
}
func createTable(columns []string) *tablewriter.Table {
func CreateTable(columns []string) *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(columns)

View File

@ -1,10 +1,10 @@
package cli
package internal
import (
"github.com/urfave/cli/v2"
)
const emptyArgsUsage = " " // Removes "[arguments]" from help. Empty str's are ignored
const EmptyArgsUsage = " " // Removes "[arguments]" from help. Empty str's are ignored
// Flags

View File

@ -1,4 +1,4 @@
package cli
package internal
import (
"os"
@ -8,7 +8,7 @@ import (
)
// showSubcommandHelpAndError exits the program on error, logs the error to the terminal, and shows the help command.
func showSubcommandHelpAndError(c *cli.Context, err interface{}) {
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) {
cli.ShowSubcommandHelp(c)
logrus.Error(err)
os.Exit(1)

View File

@ -1,4 +1,4 @@
package cli
package recipe
import (
"fmt"
@ -8,6 +8,7 @@ import (
"text/template"
"coopcloud.tech/abra/catalogue"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/config"
"github.com/go-git/go-git/v5"
@ -27,7 +28,7 @@ var recipeListCommand = &cli.Command{
apps := catl.Flatten()
sort.Sort(catalogue.ByAppName(apps))
tableCol := []string{"Name", "Category", "Status"}
table := createTable(tableCol)
table := formatter.CreateTable(tableCol)
for _, app := range apps {
status := fmt.Sprintf("%v", app.Features.Status)
tableRow := []string{app.Name, app.Category, status}
@ -57,7 +58,7 @@ var recipeVersionCommand = &cli.Command{
if app, ok := catalogue[recipe]; ok {
tableCol := []string{"Version", "Service", "Image", "Digest"}
table := createTable(tableCol)
table := formatter.CreateTable(tableCol)
for version := range app.Versions {
for service := range app.Versions[version] {
meta := app.Versions[version][service]

View File

@ -1,267 +0,0 @@
package cli
import (
"context"
"fmt"
"net"
"strings"
"coopcloud.tech/abra/client"
"coopcloud.tech/abra/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List locally-defined servers.",
ArgsUsage: emptyArgsUsage,
HideHelp: true,
Action: func(c *cli.Context) error {
dockerContextStore := client.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
logrus.Fatal(err)
}
tableColumns := []string{"Name", "Connection"}
table := createTable(tableColumns)
defer table.Render()
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
}
for _, serverName := range serverNames {
var row []string
for _, ctx := range contexts {
endpoint, err := client.GetContextEndpoint(ctx)
if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely
continue
}
if ctx.Name == serverName {
row = []string{serverName, endpoint}
}
}
if len(row) == 0 {
row = []string{serverName, "UNKNOWN"}
}
table.Append(row)
}
return nil
},
}
var serverAddCommand = &cli.Command{
Name: "add",
Usage: "Add a new server, reachable on <host>.",
ArgsUsage: "<host> [<user>] [<port>]",
Description: "[<user>], [<port>] SSH connection details",
Action: func(c *cli.Context) error {
arg_len := c.Args().Len()
args := c.Args().Slice()
if arg_len < 3 {
args = append(args, make([]string, 3-arg_len)...)
}
if err := client.CreateContext(args[0], args[1], args[2]); err != nil {
logrus.Fatal(err)
}
fmt.Println(args[0])
return nil
},
}
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.
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.
Your token can be loaded from the environment using the HCLOUD_API_TOKEN
environment variable or otherwise passing the "--env/-e" 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_API_TOKEN"},
Destination: &HetznerCloudAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
cli.ShowSubcommandHelp(c)
return nil
}
ctx := context.Background()
client := hcloud.NewClient(hcloud.WithToken(HetznerCloudAPIToken))
// var sshkeys []hcloud.SSHKey
// for _, sshkey := range HetznerCloudSSHKeys {
// sshkeys = append(sshkeys, hcloud.SSHKey{Name: sshkey})
// }
// TODO: finish passing arguments
serverOpts := hcloud.ServerCreateOpts{
Name: name,
ServerType: &hcloud.ServerType{Name: HetznerCloudType},
Image: &hcloud.Image{Name: HetznerCloudImage},
// SSHKeys: HetznerCloudSSHKeys,
// Location: HetznerCloudLocation,
}
_, _, err := client.Server.Create(ctx, serverOpts)
if err != nil {
logrus.Fatal(err)
}
return nil
},
}
var serverNewCommand = &cli.Command{
Name: "new",
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.",
ArgsUsage: "<provider>",
Subcommands: []*cli.Command{
serverNewHetznerCloudCommand,
},
}
var serverRemoveCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm", "delete"},
Usage: "Remove a locally-defined server",
HideHelp: true,
Action: func(c *cli.Context) error {
server := c.Args().First()
if server == "" {
cli.ShowSubcommandHelp(c)
return nil
}
if err := client.DeleteContext(server); err != nil {
logrus.Fatal(err)
}
return nil
},
}
var serverInitCommand = &cli.Command{
Name: "init",
Usage: "Initialise server for deploying apps",
HideHelp: true,
ArgsUsage: "<host>",
Description: `
Initialise swarm mode on the target <host>.
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 {
host := c.Args().First()
if host == "" {
cli.ShowSubcommandHelp(c)
return nil
}
cl, err := client.NewClientWithContext(host)
if err != nil {
return err
}
var ipv4 net.IP
ips, _ := net.LookupIP(host)
for _, ip := range ips {
ipv4 = ip.To4()
}
if string(ipv4) == "" {
return fmt.Errorf("unable to retrieve ipv4 address for %s", host)
}
ctx := context.Background()
initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: string(ipv4),
}
if _, err := cl.SwarmInit(ctx, initReq); err != nil {
return err
}
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
return err
}
return nil
},
}
// Reminder: The list commands are in is the order they appear in the help menu
var ServerCommand = &cli.Command{
Name: "server",
ArgsUsage: "<host>",
Usage: "Manage the servers that host your apps",
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.
`,
Subcommands: []*cli.Command{
serverNewCommand,
serverInitCommand,
serverAddCommand,
serverListCommand,
serverRemoveCommand,
},
}

28
cli/server/add.go Normal file
View File

@ -0,0 +1,28 @@
package server
import (
"fmt"
"coopcloud.tech/abra/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverAddCommand = &cli.Command{
Name: "add",
Usage: "Add a new server, reachable on <host>.",
ArgsUsage: "<host> [<user>] [<port>]",
Description: "[<user>], [<port>] SSH connection details",
Action: func(c *cli.Context) error {
arg_len := c.Args().Len()
args := c.Args().Slice()
if arg_len < 3 {
args = append(args, make([]string, 3-arg_len)...)
}
if err := client.CreateContext(args[0], args[1], args[2]); err != nil {
logrus.Fatal(err)
}
fmt.Println(args[0])
return nil
},
}

64
cli/server/init.go Normal file
View File

@ -0,0 +1,64 @@
package server
import (
"context"
"fmt"
"net"
"coopcloud.tech/abra/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/urfave/cli/v2"
)
var serverInitCommand = &cli.Command{
Name: "init",
Usage: "Initialise server for deploying apps",
HideHelp: true,
ArgsUsage: "<host>",
Description: `
Initialise swarm mode on the target <host>.
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 {
host := c.Args().First()
if host == "" {
cli.ShowSubcommandHelp(c)
return nil
}
cl, err := client.NewClientWithContext(host)
if err != nil {
return err
}
var ipv4 net.IP
ips, _ := net.LookupIP(host)
for _, ip := range ips {
ipv4 = ip.To4()
}
if string(ipv4) == "" {
return fmt.Errorf("unable to retrieve ipv4 address for %s", host)
}
ctx := context.Background()
initReq := swarm.InitRequest{
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: string(ipv4),
}
if _, err := cl.SwarmInit(ctx, initReq); err != nil {
return err
}
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(ctx, "proxy", netOpts); err != nil {
return err
}
return nil
},
}

55
cli/server/list.go Normal file
View File

@ -0,0 +1,55 @@
package server
import (
"strings"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/client"
"coopcloud.tech/abra/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List locally-defined servers.",
ArgsUsage: " ",
HideHelp: true,
Action: func(c *cli.Context) error {
dockerContextStore := client.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
logrus.Fatal(err)
}
tableColumns := []string{"Name", "Connection"}
table := formatter.CreateTable(tableColumns)
defer table.Render()
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
}
for _, serverName := range serverNames {
var row []string
for _, ctx := range contexts {
endpoint, err := client.GetContextEndpoint(ctx)
if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely
continue
}
if ctx.Name == serverName {
row = []string{serverName, endpoint}
}
}
if len(row) == 0 {
row = []string{serverName, "UNKNOWN"}
}
table.Append(row)
}
return nil
},
}

106
cli/server/new.go Normal file
View File

@ -0,0 +1,106 @@
package server
import (
"context"
"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.
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.
Your token can be loaded from the environment using the HCLOUD_API_TOKEN
environment variable or otherwise passing the "--env/-e" 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_API_TOKEN"},
Destination: &HetznerCloudAPIToken,
},
},
Action: func(c *cli.Context) error {
name := c.Args().First()
if name == "" {
cli.ShowSubcommandHelp(c)
return nil
}
ctx := context.Background()
client := hcloud.NewClient(hcloud.WithToken(HetznerCloudAPIToken))
// var sshkeys []hcloud.SSHKey
// for _, sshkey := range HetznerCloudSSHKeys {
// sshkeys = append(sshkeys, hcloud.SSHKey{Name: sshkey})
// }
// TODO: finish passing arguments
serverOpts := hcloud.ServerCreateOpts{
Name: name,
ServerType: &hcloud.ServerType{Name: HetznerCloudType},
Image: &hcloud.Image{Name: HetznerCloudImage},
// SSHKeys: HetznerCloudSSHKeys,
// Location: HetznerCloudLocation,
}
_, _, err := client.Server.Create(ctx, serverOpts)
if err != nil {
logrus.Fatal(err)
}
return nil
},
}
var serverNewCommand = &cli.Command{
Name: "new",
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.",
ArgsUsage: "<provider>",
Subcommands: []*cli.Command{
serverNewHetznerCloudCommand,
},
}

25
cli/server/remove.go Normal file
View File

@ -0,0 +1,25 @@
package server
import (
"coopcloud.tech/abra/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var serverRemoveCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm", "delete"},
Usage: "Remove a locally-defined server",
HideHelp: true,
Action: func(c *cli.Context) error {
server := c.Args().First()
if server == "" {
cli.ShowSubcommandHelp(c)
return nil
}
if err := client.DeleteContext(server); err != nil {
logrus.Fatal(err)
}
return nil
},
}

26
cli/server/server.go Normal file
View File

@ -0,0 +1,26 @@
package server
import (
"github.com/urfave/cli/v2"
)
// Reminder: The list commands are in is the order they appear in the help menu
var ServerCommand = &cli.Command{
Name: "server",
ArgsUsage: "<host>",
Usage: "Manage the servers that host your apps",
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.
`,
Subcommands: []*cli.Command{
serverNewCommand,
serverInitCommand,
serverAddCommand,
serverListCommand,
serverRemoveCommand,
},
}