package server import ( "os" "path/filepath" "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/log" "coopcloud.tech/abra/pkg/server" sshPkg "coopcloud.tech/abra/pkg/ssh" "github.com/spf13/cobra" ) var ServerAddCommand = &cobra.Command{ Use: "add [[server] | --local] [flags]", Aliases: []string{"a"}, Short: "Add a new server", Long: `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: " 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("cannot use [server] and --local together") } if len(args) == 0 && !local { log.Fatal("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.Debugf("attempting to create client for %s", name) if _, err := client.New(name, timeout); err != nil { cleanUp(name) log.Fatal(err) } if created { log.Info("local server successfully added") } else { log.Warn("local server already exists") } return } _, err := createServerDir(name) if err != nil { log.Fatal(err) } created, err := newContext(name) if err != nil { cleanUp(name) log.Fatalf("unable to create local context: %s", err) } log.Debugf("attempting to create client for %s", name) if _, err := client.New(name, timeout); err != nil { cleanUp(name) log.Fatalf("ssh %s error: %s", name, sshPkg.Fatal(name, err)) } if created { log.Infof("%s successfully added", name) if _, err := dns.EnsureIPv4(name); err != nil { log.Warnf("unable to resolve IPv4 for %s", name) } return } log.Warnf("%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.Debugf("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.Fatalf("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err) } if len(files) > 0 { log.Debugf("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir) return } if err := os.RemoveAll(serverDir); err != nil { log.Fatalf("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.Debugf("context for %s already exists", name) return false, nil } } log.Debugf("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.Debugf("server dir for %s already created", name) return false, nil } return true, nil } var ( local bool ) func init() { ServerAddCommand.Flags().BoolVarP( &local, "local", "l", false, "use local server", ) }