abra/cli/app.go

321 lines
7.6 KiB
Go

package cli
import (
"context"
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/catalogue"
"coopcloud.tech/abra/client"
"coopcloud.tech/abra/config"
"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/schultz-is/passgen"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var appNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new app of <type>",
Description: `
This command takes a recipe and uses it to cook up a new app. This 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" 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" to store these generated passwords
locally in a pass store (see passwordstore.org for more).
`,
Flags: []cli.Flag{
ServerFlag,
DomainFlag,
AppNameFlag,
PassFlag,
SecretsFlag,
},
ArgsUsage: "<type>",
Action: func(c *cli.Context) error {
appType := c.Args().First()
if appType == "" {
cli.ShowSubcommandHelp(c)
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]
var latestVersion string
for tag := range app.Versions {
// apps.json versions are sorted so the last key is latest
latestVersion = tag
}
app.EnsureExists()
if err := app.EnsureVersion(latestVersion); err != nil {
logrus.Fatal(err)
}
servers := appFiles.GetServers()
if Server == "" {
prompt := &survey.Select{
Message: "Select server to create this app for:",
Options: servers,
}
if err := survey.AskOne(prompt, &Server); err != nil {
logrus.Fatal(err)
}
}
if Domain == "" {
prompt := &survey.Input{
Message: "Specify domain for this app:",
}
if err := survey.AskOne(prompt, &Domain); err != nil {
logrus.Fatal(err)
}
}
if AppName == "" {
prompt := &survey.Input{
Message: "Specify app name:",
}
if err := survey.AskOne(prompt, &AppName); err != nil {
logrus.Fatal(err)
}
}
// convert name and truncate or error out if needed
// cp over env.sample and do some sed/replacing
// generate secrets if asked to do so
// save them in a pass store if asked to do so
// Output some instructions on how to deploy this thing
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)
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 {
passwords, err := passgen.GeneratePassphrases(
1,
passgen.PassphraseWordCountDefault,
rune('-'),
passgen.PassphraseCasingDefault,
passgen.WordListDefault,
)
if err != nil {
logrus.Fatal(err)
}
for _, password := range passwords {
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,
},
}