feat: support toggling servers on/off in configuration (#594) (#612)

Jonatan Wallmander and Jonatan Wallmander created

To test this, tests added the new function:

  `ensureservernotrunning [SERVICE_NAME]`

Co-authored-by: Jonatan Wallmander <jonatan.wallmander@kdab.com>

Change summary

cmd/soft/serve/server.go                                | 73 ++++++----
pkg/config/config.go                                    | 20 +++
testscript/script_test.go                               | 57 ++++++--
testscript/testdata/config-servers-git_disabled.txtar   | 18 ++
testscript/testdata/config-servers-http_disabled.txtar  | 19 ++
testscript/testdata/config-servers-ssh_disabled.txtar   | 18 ++
testscript/testdata/config-servers-stats_disabled.txtar | 18 ++
7 files changed, 181 insertions(+), 42 deletions(-)

Detailed changes

cmd/soft/serve/server.go 🔗

@@ -93,34 +93,51 @@ func NewServer(ctx context.Context) (*Server, error) {
 // Start starts the SSH server.
 func (s *Server) Start() error {
 	errg, _ := errgroup.WithContext(s.ctx)
-	errg.Go(func() error {
-		s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
-		if err := s.GitDaemon.ListenAndServe(); !errors.Is(err, daemon.ErrServerClosed) {
-			return err
-		}
-		return nil
-	})
-	errg.Go(func() error {
-		s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
-		if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
-			return err
-		}
-		return nil
-	})
-	errg.Go(func() error {
-		s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
-		if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {
-			return err
-		}
-		return nil
-	})
-	errg.Go(func() error {
-		s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
-		if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
-			return err
-		}
-		return nil
-	})
+
+	// optionally start the SSH server
+	if s.Config.SSH.Enabled {
+		errg.Go(func() error {
+			s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
+			if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {
+				return err
+			}
+			return nil
+		})
+	}
+
+	// optionally start the git daemon
+	if s.Config.Git.Enabled {
+		errg.Go(func() error {
+			s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
+			if err := s.GitDaemon.ListenAndServe(); !errors.Is(err, daemon.ErrServerClosed) {
+				return err
+			}
+			return nil
+		})
+	}
+
+	// optionally start the HTTP server
+	if s.Config.HTTP.Enabled {
+		errg.Go(func() error {
+			s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
+			if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
+				return err
+			}
+			return nil
+		})
+	}
+
+	// optionally start the Stats server
+	if s.Config.Stats.Enabled {
+		errg.Go(func() error {
+			s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
+			if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
+				return err
+			}
+			return nil
+		})
+	}
+
 	errg.Go(func() error {
 		s.Cron.Start()
 		return nil

pkg/config/config.go 🔗

@@ -18,6 +18,9 @@ var binPath = "soft"
 
 // SSHConfig is the configuration for the SSH server.
 type SSHConfig struct {
+	// Enabled toggles the SSH server on/off
+	Enabled bool `env:"ENABLED" yaml:"enabled"`
+
 	// ListenAddr is the address on which the SSH server will listen.
 	ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
 
@@ -39,6 +42,9 @@ type SSHConfig struct {
 
 // GitConfig is the Git daemon configuration for the server.
 type GitConfig struct {
+	// Enabled toggles the Git daemon on/off
+	Enabled bool `env:"ENABLED" yaml:"enabled"`
+
 	// ListenAddr is the address on which the Git daemon will listen.
 	ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
 
@@ -57,6 +63,9 @@ type GitConfig struct {
 
 // HTTPConfig is the HTTP configuration for the server.
 type HTTPConfig struct {
+	// Enabled toggles the HTTP server on/off
+	Enabled bool `env:"ENABLED" yaml:"enabled"`
+
 	// ListenAddr is the address on which the HTTP server will listen.
 	ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
 
@@ -72,6 +81,9 @@ type HTTPConfig struct {
 
 // StatsConfig is the configuration for the stats server.
 type StatsConfig struct {
+	// Enabled toggles the Stats server on/off
+	Enabled bool `env:"ENABLED" yaml:"enabled"`
+
 	// ListenAddr is the address on which the stats server will listen.
 	ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
 }
@@ -165,21 +177,25 @@ func (c *Config) Environ() []string {
 		fmt.Sprintf("SOFT_SERVE_DATA_PATH=%s", c.DataPath),
 		fmt.Sprintf("SOFT_SERVE_NAME=%s", c.Name),
 		fmt.Sprintf("SOFT_SERVE_INITIAL_ADMIN_KEYS=%s", strings.Join(c.InitialAdminKeys, "\n")),
+		fmt.Sprintf("SOFT_SERVE_SSH_ENABLED=%t", c.SSH.Enabled),
 		fmt.Sprintf("SOFT_SERVE_SSH_LISTEN_ADDR=%s", c.SSH.ListenAddr),
 		fmt.Sprintf("SOFT_SERVE_SSH_PUBLIC_URL=%s", c.SSH.PublicURL),
 		fmt.Sprintf("SOFT_SERVE_SSH_KEY_PATH=%s", c.SSH.KeyPath),
 		fmt.Sprintf("SOFT_SERVE_SSH_CLIENT_KEY_PATH=%s", c.SSH.ClientKeyPath),
 		fmt.Sprintf("SOFT_SERVE_SSH_MAX_TIMEOUT=%d", c.SSH.MaxTimeout),
 		fmt.Sprintf("SOFT_SERVE_SSH_IDLE_TIMEOUT=%d", c.SSH.IdleTimeout),
+		fmt.Sprintf("SOFT_SERVE_GIT_ENABLED=%t", c.Git.Enabled),
 		fmt.Sprintf("SOFT_SERVE_GIT_LISTEN_ADDR=%s", c.Git.ListenAddr),
 		fmt.Sprintf("SOFT_SERVE_GIT_PUBLIC_URL=%s", c.Git.PublicURL),
 		fmt.Sprintf("SOFT_SERVE_GIT_MAX_TIMEOUT=%d", c.Git.MaxTimeout),
 		fmt.Sprintf("SOFT_SERVE_GIT_IDLE_TIMEOUT=%d", c.Git.IdleTimeout),
 		fmt.Sprintf("SOFT_SERVE_GIT_MAX_CONNECTIONS=%d", c.Git.MaxConnections),
+		fmt.Sprintf("SOFT_SERVE_HTTP_ENABLED=%t", c.HTTP.Enabled),
 		fmt.Sprintf("SOFT_SERVE_HTTP_LISTEN_ADDR=%s", c.HTTP.ListenAddr),
 		fmt.Sprintf("SOFT_SERVE_HTTP_TLS_KEY_PATH=%s", c.HTTP.TLSKeyPath),
 		fmt.Sprintf("SOFT_SERVE_HTTP_TLS_CERT_PATH=%s", c.HTTP.TLSCertPath),
 		fmt.Sprintf("SOFT_SERVE_HTTP_PUBLIC_URL=%s", c.HTTP.PublicURL),
+		fmt.Sprintf("SOFT_SERVE_STATS_ENABLED=%t", c.Stats.Enabled),
 		fmt.Sprintf("SOFT_SERVE_STATS_LISTEN_ADDR=%s", c.Stats.ListenAddr),
 		fmt.Sprintf("SOFT_SERVE_LOG_FORMAT=%s", c.Log.Format),
 		fmt.Sprintf("SOFT_SERVE_LOG_TIME_FORMAT=%s", c.Log.TimeFormat),
@@ -318,6 +334,7 @@ func DefaultConfig() *Config {
 		Name:     "Soft Serve",
 		DataPath: DefaultDataPath(),
 		SSH: SSHConfig{
+			Enabled:       true,
 			ListenAddr:    ":23231",
 			PublicURL:     "ssh://localhost:23231",
 			KeyPath:       filepath.Join("ssh", "soft_serve_host_ed25519"),
@@ -326,6 +343,7 @@ func DefaultConfig() *Config {
 			IdleTimeout:   10 * 60, // 10 minutes
 		},
 		Git: GitConfig{
+			Enabled:        true,
 			ListenAddr:     ":9418",
 			PublicURL:      "git://localhost",
 			MaxTimeout:     0,
@@ -333,10 +351,12 @@ func DefaultConfig() *Config {
 			MaxConnections: 32,
 		},
 		HTTP: HTTPConfig{
+			Enabled:    true,
 			ListenAddr: ":23232",
 			PublicURL:  "http://localhost:23232",
 		},
 		Stats: StatsConfig{
+			Enabled:    true,
 			ListenAddr: "localhost:23233",
 		},
 		Log: LogConfig{

testscript/script_test.go 🔗

@@ -79,20 +79,21 @@ func TestScript(t *testing.T) {
 		UpdateScripts:       *update,
 		RequireExplicitExec: true,
 		Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
-			"soft":                cmdSoft("admin", admin1.Signer()),
-			"usoft":               cmdSoft("user1", user1.Signer()),
-			"git":                 cmdGit(admin1Key),
-			"ugit":                cmdGit(user1Key),
-			"curl":                cmdCurl,
-			"mkfile":              cmdMkfile,
-			"envfile":             cmdEnvfile,
-			"readfile":            cmdReadfile,
-			"dos2unix":            cmdDos2Unix,
-			"new-webhook":         cmdNewWebhook,
-			"ensureserverrunning": cmdEnsureServerRunning,
-			"stopserver":          cmdStopserver,
-			"ui":                  cmdUI(admin1.Signer()),
-			"uui":                 cmdUI(user1.Signer()),
+			"soft":                   cmdSoft("admin", admin1.Signer()),
+			"usoft":                  cmdSoft("user1", user1.Signer()),
+			"git":                    cmdGit(admin1Key),
+			"ugit":                   cmdGit(user1Key),
+			"curl":                   cmdCurl,
+			"mkfile":                 cmdMkfile,
+			"envfile":                cmdEnvfile,
+			"readfile":               cmdReadfile,
+			"dos2unix":               cmdDos2Unix,
+			"new-webhook":            cmdNewWebhook,
+			"ensureserverrunning":    cmdEnsureServerRunning,
+			"ensureservernotrunning": cmdEnsureServerNotRunning,
+			"stopserver":             cmdStopserver,
+			"ui":                     cmdUI(admin1.Signer()),
+			"uui":                    cmdUI(user1.Signer()),
 		},
 		Setup: func(e *testscript.Env) error {
 			// Add binPath to PATH
@@ -112,6 +113,8 @@ func TestScript(t *testing.T) {
 			e.Setenv("DATA_PATH", data)
 			e.Setenv("SSH_PORT", fmt.Sprintf("%d", sshPort))
 			e.Setenv("HTTP_PORT", fmt.Sprintf("%d", httpPort))
+			e.Setenv("STATS_PORT", fmt.Sprintf("%d", statsPort))
+			e.Setenv("GIT_PORT", fmt.Sprintf("%d", gitPort))
 			e.Setenv("ADMIN1_AUTHORIZED_KEY", admin1.AuthorizedKey())
 			e.Setenv("ADMIN2_AUTHORIZED_KEY", admin2.AuthorizedKey())
 			e.Setenv("USER1_AUTHORIZED_KEY", user1.AuthorizedKey())
@@ -496,6 +499,32 @@ func cmdEnsureServerRunning(ts *testscript.TestScript, neg bool, args []string)
 	}
 }
 
+func cmdEnsureServerNotRunning(ts *testscript.TestScript, neg bool, args []string) {
+	if len(args) < 1 {
+		ts.Fatalf("Must supply a TCP port of one of the services to connect to. " +
+			"These are set as env vars as they are randomized. " +
+			"Example usage: \"cmdensureservernotrunning SSH_PORT\"\n" +
+			"Valid values for the env var: SSH_PORT|HTTP_PORT|GIT_PORT|STATS_PORT")
+	}
+
+	port := ts.Getenv(args[0])
+
+	// verify that the server is not up
+	addr := net.JoinHostPort("localhost", port)
+	for {
+		conn, _ := net.DialTimeout(
+			"tcp",
+			addr,
+			time.Second,
+		)
+		if conn != nil {
+			ts.Fatalf("server is running on port %s while it should not be running", port)
+			conn.Close()
+		}
+		break
+	}
+}
+
 func cmdStopserver(ts *testscript.TestScript, neg bool, args []string) {
 	// stop the server
 	resp, err := http.DefaultClient.Head(fmt.Sprintf("%s/__stop", ts.Getenv("SOFT_SERVE_HTTP_PUBLIC_URL")))

testscript/testdata/config-servers-git_disabled.txtar 🔗

@@ -0,0 +1,18 @@
+# vi: set ft=conf
+
+# disable git listening
+env SOFT_SERVE_SSH_ENABLED=true
+env SOFT_SERVE_GIT_ENABLED=false
+env SOFT_SERVE_HTTP_ENABLED=true
+env SOFT_SERVE_STATS_ENABLED=true
+
+# start soft serve
+exec soft serve --sync-hooks &
+
+# wait for the ssh + other servers to come up
+ensureserverrunning SSH_PORT
+ensureserverrunning HTTP_PORT
+ensureserverrunning STATS_PORT
+
+# ensure that the disabled server is not running
+ensureservernotrunning GIT_PORT

testscript/testdata/config-servers-http_disabled.txtar 🔗

@@ -0,0 +1,19 @@
+# vi: set ft=conf
+
+# disable http listening
+env SOFT_SERVE_SSH_ENABLED=true
+env SOFT_SERVE_GIT_ENABLED=true
+env SOFT_SERVE_HTTP_ENABLED=false
+env SOFT_SERVE_STATS_ENABLED=true
+
+# start soft serve
+exec soft serve --sync-hooks &
+
+# wait for the ssh + other servers to come up
+ensureserverrunning SSH_PORT
+ensureserverrunning GIT_PORT
+ensureserverrunning STATS_PORT
+
+# ensure that the disabled server is not running
+ensureservernotrunning HTTP_PORT
+

testscript/testdata/config-servers-ssh_disabled.txtar 🔗

@@ -0,0 +1,18 @@
+# vi: set ft=conf
+
+# disable ssh listening
+env SOFT_SERVE_SSH_ENABLED=false
+env SOFT_SERVE_GIT_ENABLED=true
+env SOFT_SERVE_HTTP_ENABLED=true
+env SOFT_SERVE_STATS_ENABLED=true
+
+# start soft serve
+exec soft serve --sync-hooks &
+
+# wait for the git + other servers to come up
+ensureserverrunning GIT_PORT
+ensureserverrunning HTTP_PORT
+ensureserverrunning STATS_PORT
+
+# ensure that the disabled server is not running
+ensureservernotrunning SSH_PORT

testscript/testdata/config-servers-stats_disabled.txtar 🔗

@@ -0,0 +1,18 @@
+# vi: set ft=conf
+
+# disable stats listening
+env SOFT_SERVE_SSH_ENABLED=true
+env SOFT_SERVE_GIT_ENABLED=true
+env SOFT_SERVE_HTTP_ENABLED=true
+env SOFT_SERVE_STATS_ENABLED=false
+
+# start soft serve
+exec soft serve --sync-hooks &
+
+# wait for the ssh + other servers to come up
+ensureserverrunning SSH_PORT
+ensureserverrunning GIT_PORT
+ensureserverrunning HTTP_PORT
+
+# ensure that the disabled server is not running
+ensureservernotrunning STATS_PORT