git » chasquid » commit bb1b921

Add /exit endpoint to monitoring server

author ThinkChaos
2020-10-31 20:46:47 UTC
committer Alberto Bertogli
2020-11-12 23:24:21 UTC
parent e9c6775418f9bb2c13097a5e05fa6aed4ecba0b3

Add /exit endpoint to monitoring server

Allows terminating chasquid via the network. Useful to trigger a restart
(if there is an init system to relaunch chasquid) and thus reload certificates.

Amended-by: Alberto Bertogli <albertito@blitiri.com.ar>
  Added tests, and adjusted shutdown sequence.

chasquid.go +1 -1
monitoring.go +29 -1
test/t-13-reload/run.sh +11 -0
test/util/lib.sh +9 -0

diff --git a/chasquid.go b/chasquid.go
index 32491ba..00bb2a7 100644
--- a/chasquid.go
+++ b/chasquid.go
@@ -93,7 +93,7 @@ func main() {
 	go signalHandler()
 
 	if conf.MonitoringAddress != "" {
-		launchMonitoringServer(conf)
+		go launchMonitoringServer(conf)
 	}
 
 	s := smtpsrv.NewServer()
diff --git a/monitoring.go b/monitoring.go
index 1009bce..6f6aab3 100644
--- a/monitoring.go
+++ b/monitoring.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"html/template"
@@ -49,11 +50,16 @@ func launchMonitoringServer(conf *config.Config) {
 		}
 	})
 
+	srv := &http.Server{Addr: conf.MonitoringAddress}
+
+	http.HandleFunc("/exit", exitHandler(srv))
 	http.HandleFunc("/metrics", expvarom.MetricsHandler)
 	http.HandleFunc("/debug/flags", debugFlagsHandler)
 	http.HandleFunc("/debug/config", debugConfigHandler(conf))
 
-	go http.ListenAndServe(conf.MonitoringAddress, nil)
+	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
+		log.Fatalf("Monitoring server failed: %v", err)
+	}
 }
 
 // Functions available inside the templates.
@@ -123,6 +129,28 @@ os hostname <i>{{.Hostname}}</i><br>
 </html>
 `))
 
+func exitHandler(srv *http.Server) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != "POST" {
+			http.Error(w, "Use POST method for exiting", http.StatusMethodNotAllowed)
+			return
+		}
+
+		log.Infof("Received /exit")
+		http.Error(w, "OK exiting", http.StatusOK)
+
+		// Launch srv.Shutdown asynchronously, and then exit.
+		// The http documentation says to wait for Shutdown to return before
+		// exiting, to gracefully close all ongoing requests.
+		go func() {
+			if err := srv.Shutdown(context.Background()); err != nil {
+				log.Fatalf("Monitoring server shutdown failed: %v", err)
+			}
+			os.Exit(0)
+		}()
+	}
+}
+
 func debugFlagsHandler(w http.ResponseWriter, r *http.Request) {
 	visited := make(map[string]bool)
 
diff --git a/test/t-13-reload/run.sh b/test/t-13-reload/run.sh
index a3a0486..b952497 100755
--- a/test/t-13-reload/run.sh
+++ b/test/t-13-reload/run.sh
@@ -62,4 +62,15 @@ then
 	fail "new chasquid log did not have the expected entry"
 fi
 
+
+# Test that we can make the server exit using the /exit endpoint.
+# First, a GET should fail with status 405.
+fexp http://localhost:1099/exit -status 405
+
+# A POST should succeed, return an OK body, and the daemon should
+# eventually exit.
+CHASQUID_PID=$(pgrep -s 0 chasquid)
+fexp http://localhost:1099/exit -method POST -bodyre "OK"
+wait_until ! kill -s 0 $CHASQUID_PID 2> /dev/null
+
 success
diff --git a/test/util/lib.sh b/test/util/lib.sh
index 10870b6..f637655 100644
--- a/test/util/lib.sh
+++ b/test/util/lib.sh
@@ -153,6 +153,15 @@ function wait_for_file() {
 	done
 }
 
+function wait_until() {
+	while true; do
+		if eval "$@"; then
+			return 0
+		fi
+		sleep 0.05
+	done
+}
+
 # Generate certs for the given hostname.
 function generate_certs_for() {
 	CONFDIR="${CONFDIR:-config}"