package server import ( "os" "path/filepath" "strings" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" contextPkg "coopcloud.tech/abra/pkg/context" "coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/server" sshPkg "coopcloud.tech/abra/pkg/ssh" "github.com/spf13/cobra" ) // translators: `abra server add` aliases. use a comma separated list of // aliases with no spaces in between var serverAddAliases = i18n.G("a") var ServerAddCommand = &cobra.Command{ // translators: `server add` command Use: i18n.G("add [[server] | --local] [flags]"), Aliases: strings.Split(serverAddAliases, ","), // translators: Short description for `server add` command Short: i18n.G("Add a new server"), Long: i18n.G(`Add a new server to your configuration so that it can be managed by Abra. Abra relies on the standard SSH command-line and ~/.ssh/config for client connection details. You must configure an entry per-host in your ~/.ssh/config for each server: Host 1312.net 1312 Hostname 1312.net User antifa Port 12345 IdentityFile ~/.ssh/antifa@somewhere If "--local" is passed, then Abra assumes that the current local server is intended as the target server. This is useful when you want to have your entire Co-op Cloud config located on the server itself, and not on your local developer machine. The domain is then set to "default".`), Example: i18n.G(" abra server add 1312.net"), Args: cobra.RangeArgs(0, 1), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if !local { return autocomplete.ServerNameComplete() } return nil, cobra.ShellCompDirectiveDefault }, Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 && local { log.Fatal(i18n.G("cannot use [server] and --local together")) } if len(args) == 0 && !local { log.Fatal(i18n.G("missing argument or --local/-l flag")) } name := "default" if !local { name = internal.ValidateDomain(args) } // NOTE(d1): reasonable 5 second timeout for connections which can't // succeed. The connection is attempted twice, so this results in 10 // seconds. timeout := client.WithTimeout(5) if local { created, err := createServerDir(name) if err != nil { log.Fatal(err) } log.Debug(i18n.G("attempting to create client for %s", name)) if _, err := client.New(name, timeout); err != nil { cleanUp(name) log.Fatal(err) } if created { log.Info(i18n.G("local server successfully added")) } else { log.Warn(i18n.G("local server already exists")) } return } _, err := createServerDir(name) if err != nil { log.Fatal(err) } created, err := newContext(name) if err != nil { cleanUp(name) log.Fatal(i18n.G("unable to create local context: %s", err)) } log.Debug(i18n.G("attempting to create client for %s", name)) if _, err := client.New(name, timeout); err != nil { cleanUp(name) log.Fatal(i18n.G("ssh %s error: %s", name, sshPkg.Fatal(name, err))) } if created { log.Info(i18n.G("%s successfully added", name)) if _, err := dns.EnsureIPv4(name); err != nil { log.Warn(i18n.G("unable to resolve IPv4 for %s", name)) } return } log.Warn(i18n.G("%s already exists", name)) }, } // cleanUp cleans up the partially created context/client details for a failed // "server add" attempt. func cleanUp(name string) { if name != "default" { log.Debug(i18n.G("serverAdd: cleanUp: cleaning up context for %s", name)) if err := client.DeleteContext(name); err != nil { log.Fatal(err) } } serverDir := filepath.Join(config.SERVERS_DIR, name) files, err := config.GetAllFilesInDirectory(serverDir) if err != nil { log.Fatal(i18n.G("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err)) } if len(files) > 0 { log.Debug(i18n.G("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir)) return } if err := os.RemoveAll(serverDir); err != nil { log.Fatal(i18n.G("serverAdd: cleanUp: failed to remove %s: %s", serverDir, err)) } } // newContext creates a new internal Docker context for a server. This is how // Docker manages SSH connection details. These are stored to disk in // ~/.docker. Abra can manage this completely for the user, so it's an // implementation detail. func newContext(name string) (bool, error) { store := contextPkg.NewDefaultDockerContextStore() contexts, err := store.Store.List() if err != nil { return false, err } for _, context := range contexts { if context.Name == name { log.Debug(i18n.G("context for %s already exists", name)) return false, nil } } log.Debugf(i18n.G("creating context with domain %s", name)) if err := client.CreateContext(name); err != nil { return false, nil } return true, nil } // createServerDir creates the ~/.abra/servers/... directory for a new server. func createServerDir(name string) (bool, error) { if err := server.CreateServerDir(name); err != nil { if !os.IsExist(err) { return false, err } log.Debug(i18n.G("server dir for %s already created", name)) return false, nil } return true, nil } var ( local bool ) func init() { ServerAddCommand.Flags().BoolVarP( &local, i18n.G("local"), i18n.G("l"), false, i18n.G("use local server"), ) }