From 30d11f48a7ae535a3f602e705f31dd8f426c0d23 Mon Sep 17 00:00:00 2001 From: Roxie Gibson Date: Mon, 2 Aug 2021 02:10:41 +0100 Subject: [PATCH] refactor: break up cli pkg into nice small chunks --- cli/app.go | 355 ------------------------------- cli/app/app.go | 32 +++ cli/app/backup.go | 11 + cli/app/check.go | 7 + cli/app/config.go | 7 + cli/app/cp.go | 8 + cli/app/deploy.go | 16 ++ cli/app/list.go | 70 ++++++ cli/app/logs.go | 8 + cli/app/new.go | 153 +++++++++++++ cli/app/ps.go | 61 ++++++ cli/app/remove.go | 11 + cli/app/restore.go | 12 ++ cli/app/rollback.go | 8 + cli/app/run.go | 15 ++ cli/app/secret.go | 25 +++ cli/app/undeploy.go | 7 + cli/cli.go | 28 +-- cli/{ => formatter}/formatter.go | 16 +- cli/{ => internal}/common.go | 4 +- cli/{ => internal}/errors.go | 4 +- cli/{ => recipe}/recipe.go | 7 +- cli/server.go | 267 ----------------------- cli/server/add.go | 28 +++ cli/server/init.go | 64 ++++++ cli/server/list.go | 55 +++++ cli/server/new.go | 106 +++++++++ cli/server/remove.go | 25 +++ cli/server/server.go | 26 +++ 29 files changed, 787 insertions(+), 649 deletions(-) delete mode 100644 cli/app.go create mode 100644 cli/app/app.go create mode 100644 cli/app/backup.go create mode 100644 cli/app/check.go create mode 100644 cli/app/config.go create mode 100644 cli/app/cp.go create mode 100644 cli/app/deploy.go create mode 100644 cli/app/list.go create mode 100644 cli/app/logs.go create mode 100644 cli/app/new.go create mode 100644 cli/app/ps.go create mode 100644 cli/app/remove.go create mode 100644 cli/app/restore.go create mode 100644 cli/app/rollback.go create mode 100644 cli/app/run.go create mode 100644 cli/app/secret.go create mode 100644 cli/app/undeploy.go rename cli/{ => formatter}/formatter.go (73%) rename cli/{ => internal}/common.go (97%) rename cli/{ => internal}/errors.go (76%) rename cli/{ => recipe}/recipe.go (96%) delete mode 100644 cli/server.go create mode 100644 cli/server/add.go create mode 100644 cli/server/init.go create mode 100644 cli/server/list.go create mode 100644 cli/server/new.go create mode 100644 cli/server/remove.go create mode 100644 cli/server/server.go diff --git a/cli/app.go b/cli/app.go deleted file mode 100644 index c7b4b4e05..000000000 --- a/cli/app.go +++ /dev/null @@ -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 " to do so. - -You can see what apps can be created (i.e. values for the 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: "", - 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: " []", -} -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: " ", -} -var appConfigCommand = &cli.Command{ - Name: "config", -} -var appLogsCommand = &cli.Command{ - Name: "logs", - ArgsUsage: "[]", -} - -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: " ...", -} - -var appRollbackCommand = &cli.Command{ - Name: "rollback", - ArgsUsage: "[]", -} - -// 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, - }, -} diff --git a/cli/app/app.go b/cli/app/app.go new file mode 100644 index 000000000..0246a75ed --- /dev/null +++ b/cli/app/app.go @@ -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, + }, +} diff --git a/cli/app/backup.go b/cli/app/backup.go new file mode 100644 index 000000000..22ae9d034 --- /dev/null +++ b/cli/app/backup.go @@ -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}, +} diff --git a/cli/app/check.go b/cli/app/check.go new file mode 100644 index 000000000..50cb5e9e7 --- /dev/null +++ b/cli/app/check.go @@ -0,0 +1,7 @@ +package app + +import "github.com/urfave/cli/v2" + +var appCheckCommand = &cli.Command{ + Name: "check", +} diff --git a/cli/app/config.go b/cli/app/config.go new file mode 100644 index 000000000..eae01cef2 --- /dev/null +++ b/cli/app/config.go @@ -0,0 +1,7 @@ +package app + +import "github.com/urfave/cli/v2" + +var appConfigCommand = &cli.Command{ + Name: "config", +} diff --git a/cli/app/cp.go b/cli/app/cp.go new file mode 100644 index 000000000..e39d26b3a --- /dev/null +++ b/cli/app/cp.go @@ -0,0 +1,8 @@ +package app + +import "github.com/urfave/cli/v2" + +var appCpCommand = &cli.Command{ + Name: "cp", + ArgsUsage: " ", +} diff --git a/cli/app/deploy.go b/cli/app/deploy.go new file mode 100644 index 000000000..c9ccbce45 --- /dev/null +++ b/cli/app/deploy.go @@ -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, + }, +} diff --git a/cli/app/list.go b/cli/app/list.go new file mode 100644 index 000000000..f971bb575 --- /dev/null +++ b/cli/app/list.go @@ -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 + }, +} diff --git a/cli/app/logs.go b/cli/app/logs.go new file mode 100644 index 000000000..8ca0909cd --- /dev/null +++ b/cli/app/logs.go @@ -0,0 +1,8 @@ +package app + +import "github.com/urfave/cli/v2" + +var appLogsCommand = &cli.Command{ + Name: "logs", + ArgsUsage: "[]", +} diff --git a/cli/app/new.go b/cli/app/new.go new file mode 100644 index 000000000..5bdd6bb3b --- /dev/null +++ b/cli/app/new.go @@ -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 " to do so. + +You can see what apps can be created (i.e. values for the 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: "", + 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 + }, +} diff --git a/cli/app/ps.go b/cli/app/ps.go new file mode 100644 index 000000000..6483c699a --- /dev/null +++ b/cli/app/ps.go @@ -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 + }, +} diff --git a/cli/app/remove.go b/cli/app/remove.go new file mode 100644 index 000000000..06430e199 --- /dev/null +++ b/cli/app/remove.go @@ -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}, +} diff --git a/cli/app/restore.go b/cli/app/restore.go new file mode 100644 index 000000000..e56606d7d --- /dev/null +++ b/cli/app/restore.go @@ -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: " []", +} diff --git a/cli/app/rollback.go b/cli/app/rollback.go new file mode 100644 index 000000000..0c21e7589 --- /dev/null +++ b/cli/app/rollback.go @@ -0,0 +1,8 @@ +package app + +import "github.com/urfave/cli/v2" + +var appRollbackCommand = &cli.Command{ + Name: "rollback", + ArgsUsage: "[]", +} diff --git a/cli/app/run.go b/cli/app/run.go new file mode 100644 index 000000000..83f48a6c6 --- /dev/null +++ b/cli/app/run.go @@ -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: " ...", +} diff --git a/cli/app/secret.go b/cli/app/secret.go new file mode 100644 index 000000000..fdf71a422 --- /dev/null +++ b/cli/app/secret.go @@ -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 + }, +} diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go new file mode 100644 index 000000000..a4177c2b7 --- /dev/null +++ b/cli/app/undeploy.go @@ -0,0 +1,7 @@ +package app + +import "github.com/urfave/cli/v2" + +var appUndeployCommand = &cli.Command{ + Name: "undeploy", +} diff --git a/cli/cli.go b/cli/cli.go index c1612e291..c893bcc1f 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -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, }, } diff --git a/cli/formatter.go b/cli/formatter/formatter.go similarity index 73% rename from cli/formatter.go rename to cli/formatter/formatter.go index 4a2d09412..5069c9d99 100644 --- a/cli/formatter.go +++ b/cli/formatter/formatter.go @@ -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) diff --git a/cli/common.go b/cli/internal/common.go similarity index 97% rename from cli/common.go rename to cli/internal/common.go index cbdf068ac..6790417f5 100644 --- a/cli/common.go +++ b/cli/internal/common.go @@ -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 diff --git a/cli/errors.go b/cli/internal/errors.go similarity index 76% rename from cli/errors.go rename to cli/internal/errors.go index 803771526..49070b5ca 100644 --- a/cli/errors.go +++ b/cli/internal/errors.go @@ -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) diff --git a/cli/recipe.go b/cli/recipe/recipe.go similarity index 96% rename from cli/recipe.go rename to cli/recipe/recipe.go index 7cf1ed63c..65bb899b3 100644 --- a/cli/recipe.go +++ b/cli/recipe/recipe.go @@ -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] diff --git a/cli/server.go b/cli/server.go deleted file mode 100644 index a4f0ae8dc..000000000 --- a/cli/server.go +++ /dev/null @@ -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 .", - ArgsUsage: " [] []", - Description: "[], [] 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: "", - 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: "", - 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: "", - Description: ` -Initialise swarm mode on the target . - -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: "", - 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, - }, -} diff --git a/cli/server/add.go b/cli/server/add.go new file mode 100644 index 000000000..59844fbf6 --- /dev/null +++ b/cli/server/add.go @@ -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 .", + ArgsUsage: " [] []", + Description: "[], [] 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 + }, +} diff --git a/cli/server/init.go b/cli/server/init.go new file mode 100644 index 000000000..00953f895 --- /dev/null +++ b/cli/server/init.go @@ -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: "", + Description: ` +Initialise swarm mode on the target . + +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 + }, +} diff --git a/cli/server/list.go b/cli/server/list.go new file mode 100644 index 000000000..63ec32cb2 --- /dev/null +++ b/cli/server/list.go @@ -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 + + }, +} diff --git a/cli/server/new.go b/cli/server/new.go new file mode 100644 index 000000000..d72e85bfe --- /dev/null +++ b/cli/server/new.go @@ -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: "", + 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: "", + Subcommands: []*cli.Command{ + serverNewHetznerCloudCommand, + }, +} diff --git a/cli/server/remove.go b/cli/server/remove.go new file mode 100644 index 000000000..efef2246a --- /dev/null +++ b/cli/server/remove.go @@ -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 + }, +} diff --git a/cli/server/server.go b/cli/server/server.go new file mode 100644 index 000000000..851c7a4e9 --- /dev/null +++ b/cli/server/server.go @@ -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: "", + 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, + }, +}