Skip to content

Commit

Permalink
feat: support toggling servers on/off in configuration (#594)
Browse files Browse the repository at this point in the history
This also fixes a bug in the git daemon which would listen on TCP as it was created.
Now it only listens when Start() has been called.
To facilitate testing of this, tests now use the more granular

  `ensureserverrunning [SERVICE_NAME]` instead of the

previous, more crude `waitforserver`.
  • Loading branch information
Jonatan Wallmander committed Nov 19, 2024
1 parent 682dccb commit 980acf4
Show file tree
Hide file tree
Showing 30 changed files with 263 additions and 95 deletions.
73 changes: 45 additions & 28 deletions cmd/soft/serve/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Start(); !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

Check failure on line 102 in cmd/soft/serve/server.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func (*github.com/charmbracelet/soft-serve/pkg/ssh.SSHServer).ListenAndServe() error (wrapcheck)
}
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.Start(); !errors.Is(err, daemon.ErrServerClosed) {
return err

Check failure on line 113 in cmd/soft/serve/server.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func (*github.com/charmbracelet/soft-serve/pkg/daemon.GitDaemon).Start() error (wrapcheck)
}
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

Check failure on line 124 in cmd/soft/serve/server.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func (*github.com/charmbracelet/soft-serve/pkg/web.HTTPServer).ListenAndServe() error (wrapcheck)
}
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

Check failure on line 135 in cmd/soft/serve/server.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func (*github.com/charmbracelet/soft-serve/pkg/stats.StatsServer).ListenAndServe() error (wrapcheck)
}
return nil
})
}

errg.Go(func() error {
s.Cron.Start()
return nil
Expand Down
20 changes: 20 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand All @@ -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"`

Expand All @@ -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"`

Expand All @@ -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"`
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"),
Expand All @@ -326,17 +343,20 @@ func DefaultConfig() *Config {
IdleTimeout: 10 * 60, // 10 minutes
},
Git: GitConfig{
Enabled: true,
ListenAddr: ":9418",
PublicURL: "git://localhost",
MaxTimeout: 0,
IdleTimeout: 3,
MaxConnections: 32,
},
HTTP: HTTPConfig{
Enabled: true,
ListenAddr: ":23232",
PublicURL: "http://localhost:23232",
},
Stats: StatsConfig{
Enabled: true,
ListenAddr: "localhost:23233",
},
Log: LogConfig{
Expand Down
20 changes: 15 additions & 5 deletions pkg/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,21 @@ func NewGitDaemon(ctx context.Context) (*GitDaemon, error) {
conns: connections{m: make(map[net.Conn]struct{})},
logger: log.FromContext(ctx).WithPrefix("gitdaemon"),
}
listener, err := net.Listen("tcp", d.addr)
if err != nil {
return nil, err
}
d.listener = listener
return d, nil
}

// Start starts the Git TCP daemon.
func (d *GitDaemon) Start() error {
// listen on the socket
{
listener, err := net.Listen("tcp", d.addr)
if err != nil {
return err

Check failure on line 79 in pkg/daemon/daemon.go

View workflow job for this annotation

GitHub Actions / lint-soft

error returned from external package is unwrapped: sig: func net.Listen(network string, address string) (net.Listener, error) (wrapcheck)
}
d.listener = listener
}

// close eventual connections to the socket
defer d.listener.Close() // nolint: errcheck

d.wg.Add(1)
Expand Down Expand Up @@ -308,6 +313,11 @@ func (d *GitDaemon) Close() error {

// Shutdown gracefully shuts down the daemon.
func (d *GitDaemon) Shutdown(ctx context.Context) error {
// in the case when git daemon was never started
if d.listener == nil {
return nil
}

d.once.Do(func() { close(d.finished) })
err := d.listener.Close()
finished := make(chan struct{}, 1)
Expand Down
84 changes: 66 additions & 18 deletions testscript/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ var (
binPath string
)

func PrepareBuildCommand(binPath string) *exec.Cmd {
_, disableRaceSet := os.LookupEnv("DISABLE_RACE_CHECKS")
if disableRaceSet {
// don't add the -race flag
return exec.Command("go", "build", "-cover", "-o", binPath, filepath.Join("..", "cmd", "soft"))
}
return exec.Command("go", "build", "-race", "-cover", "-o", binPath, filepath.Join("..", "cmd", "soft"))
}

func TestMain(m *testing.M) {
tmp, err := os.MkdirTemp("", "soft-serve*")
if err != nil {
Expand All @@ -48,7 +57,7 @@ func TestMain(m *testing.M) {
}

// Build the soft binary with -cover flag.
cmd := exec.Command("go", "build", "-race", "-cover", "-o", binPath, filepath.Join("..", "cmd", "soft"))
cmd := PrepareBuildCommand(binPath)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "failed to build soft-serve binary: %s", err)
os.Exit(1)
Expand Down Expand Up @@ -79,20 +88,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,
"waitforserver": cmdWaitforserver,
"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
Expand All @@ -112,6 +122,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())
Expand Down Expand Up @@ -470,22 +482,58 @@ func cmdCurl(ts *testscript.TestScript, neg bool, args []string) {
check(ts, cmd.Execute(), neg)
}

func cmdWaitforserver(ts *testscript.TestScript, neg bool, args []string) {
// wait until the server is up
addr := net.JoinHostPort("localhost", ts.Getenv("SSH_PORT"))
func cmdEnsureServerRunning(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: \"cmdensureserverrunning 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 up
addr := net.JoinHostPort("localhost", port)
for {
conn, _ := net.DialTimeout(
"tcp",
addr,
time.Second,
)
if conn != nil {
ts.Logf("Server is running on port: %s", port)
conn.Close()
break
}
}
}

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")))
Expand Down
4 changes: 2 additions & 2 deletions testscript/testdata/anon-access.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

# start soft serve
exec soft serve &
# wait for server to start
waitforserver
# wait for SSH server to start
ensureserverrunning SSH_PORT

# set settings
soft settings allow-keyless true
Expand Down
18 changes: 18 additions & 0 deletions testscript/testdata/config-servers-git_disabled.txtar
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions testscript/testdata/config-servers-http_disabled.txtar
Original file line number Diff line number Diff line change
@@ -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

Loading

0 comments on commit 980acf4

Please sign in to comment.