package server import ( "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" ) var local bool var localFlag = &cli.BoolFlag{ Name: "local, 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 server to your configuration", 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. For example: Host example.com example Hostname example.com User exampleUser Port 12345 IdentityFile ~/.ssh/example@somewhere You can then add a server like so: abra server add example.com 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". You can also pass "--no-domain-checks/-D" flag to use any arbitrary name instead of a real domain. The host will be resolved with the "Hostname" entry of your ~/.ssh/config. Checks for a valid online domain will be skipped: abra server add -D example `, Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, internal.NoDomainChecksFlag, localFlag, }, Before: internal.SubCommandBefore, ArgsUsage: "", Action: func(c *cli.Context) error { if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) { err := errors.New("cannot use and --local together") internal.ShowSubcommandHelpAndError(c, err) } var name string if local { name = "default" } else { name = internal.ValidateDomain(c) } // 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 !internal.NoDomainChecks { if _, err := dns.EnsureIPv4(name); err != nil { log.Fatal(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 }, }