package server import ( "context" "errors" "os" "path/filepath" "coopcloud.tech/abra/cli/internal" "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/urfave/cli/v3" ) var local bool var localFlag = &cli.BoolFlag{ Name: "local", Aliases: []string{"l"}, Usage: "Use local server", Destination: &local, } // 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 serverAddCommand = cli.Command{ Name: "add", Aliases: []string{"a"}, Usage: "Add a new server", UsageText: "abra server add [options]", Description: `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 example.com example Hostname example.com User exampleUser Port 12345 IdentityFile ~/.ssh/example@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".`, Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, localFlag, }, Before: internal.SubCommandBefore, HideHelp: true, Action: func(ctx context.Context, cmd *cli.Command) error { if cmd.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(cmd) { err := errors.New("cannot use and --local together") internal.ShowSubcommandHelpAndError(cmd, err) } var name string if local { name = "default" } else { name = internal.ValidateDomain(cmd) } // 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 nil } if _, err := dns.EnsureIPv4(name); err != nil { log.Warn(err) } _, err := createServerDir(name) if err != nil { log.Fatal(err) } created, err := newContext(name) if err != nil { cleanUp(name) log.Fatal(err) } log.Debugf("attempting to create client for %s", name) if _, err := client.New(name, timeout); err != nil { cleanUp(name) log.Fatal(sshPkg.Fatal(name, err)) } if created { log.Infof("%s successfully added", name) } else { log.Warnf("%s already exists", name) } return nil }, }