package cli import ( "context" "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 == "" { 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] 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, }, }