212 lines
7.0 KiB
Go
212 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"git.wamblee.org/converge/pkg/models"
|
|
"git.wamblee.org/converge/pkg/server/matchmaker"
|
|
"git.wamblee.org/converge/pkg/support/websocketutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
_ "net/http/pprof"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
_ "time/tzdata"
|
|
)
|
|
|
|
func parsePublicId(path string) (publicId models.RendezVousId, _ error) {
|
|
pattern := regexp.MustCompile("/([^/]+)$")
|
|
matches := pattern.FindStringSubmatch(path)
|
|
if len(matches) != 2 {
|
|
return "", fmt.Errorf("Invalid URL path '%s'", path)
|
|
}
|
|
return models.RendezVousId(matches[1]), nil
|
|
}
|
|
|
|
func catchAllHandler(contextPath string) func(w http.ResponseWriter, r *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, contextPath+"docs", http.StatusFound)
|
|
}
|
|
}
|
|
|
|
func printHelp(msg string) {
|
|
if msg != "" {
|
|
fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", msg)
|
|
}
|
|
helpText := "Usage: converge [options]\n" +
|
|
"\n" +
|
|
"Converge server is a rendez-vous server for debugging continuous integration\n" +
|
|
"jobs be providing the capability to log into the agents where jobs are running.\n" +
|
|
"This is achieve by starting an agent in the continuous integration job\n" +
|
|
"which connects to Converge using a websocket connection. The end user also connects\n" +
|
|
"to Converge using ssh over websockets. The server then matches the end-user with\n" +
|
|
"the agent running in the continous integration job (the rendez-vous) and sets up\n" +
|
|
"an end-to-end SSH connection between end-user and agent, with the agent providing\n" +
|
|
"an embedded SSH server to provide interactive access to the end-user. This works\n" +
|
|
"both on linux and on windows.\n" +
|
|
"\n" +
|
|
"Options\n" +
|
|
"--static <contentdir>: directory where static content of converge is placed\n" +
|
|
"--downloads <downloaddir>: directory where downloads of converge are placed\n" +
|
|
"--ccntext <contextpath>: by default all content is served at /. Use this option to specify\n" +
|
|
" a different context path. For instance to host converge at a base\n" +
|
|
" URL of https://example.com/converge/, specify /converge/ (with\n" +
|
|
" trailing slash. \n" +
|
|
"--pprof: Enable the pprof endpoint at /debug/pprof"
|
|
fmt.Fprintln(os.Stderr, helpText)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func main() {
|
|
// for debugging hangs relating to using unbuffered channels
|
|
//go func() {
|
|
// buf := make([]byte, 1000000)
|
|
// n := runtime.Stack(buf, true)
|
|
// time.Sleep(10 * time.Second)
|
|
// log.Println("LOG: \n" + string(buf[:n]))
|
|
//}()
|
|
|
|
downloaddir := "."
|
|
staticdir := "../static"
|
|
contextpath := "/"
|
|
pprof := false
|
|
|
|
args := os.Args[1:]
|
|
for len(args) > 0 && strings.HasPrefix(args[0], "-") {
|
|
switch args[0] {
|
|
case "--downloads":
|
|
if len(args) <= 1 {
|
|
printHelp("The --downloads option expects an argument")
|
|
}
|
|
downloaddir = args[1]
|
|
args = args[1:]
|
|
case "--static":
|
|
if len(args) <= 1 {
|
|
printHelp("The --static option expects an argument")
|
|
}
|
|
staticdir = args[1]
|
|
args = args[1:]
|
|
case "--context":
|
|
if len(args) <= 1 {
|
|
printHelp("The --context option expects an argument")
|
|
}
|
|
contextpath = args[1]
|
|
args = args[1:]
|
|
case "--pprof":
|
|
pprof = true
|
|
default:
|
|
printHelp("Unknown option " + args[0])
|
|
}
|
|
args = args[1:]
|
|
}
|
|
log.Println("Content directory", staticdir)
|
|
|
|
if len(args) != 0 {
|
|
printHelp("")
|
|
}
|
|
|
|
// Initializing the ocre system.
|
|
// Setup of websession handling for pushing notifications to the client
|
|
// Prometheus
|
|
// And the MatchMaker. The MatchMakers sends state notifications to websessions
|
|
// and prometheus.
|
|
notifications := NewStateNotifier()
|
|
websessions := matchmaker.NewWebSessions(notifications.webNotificationChannel)
|
|
// monitoring
|
|
prometheusMux := http.NewServeMux()
|
|
setupPrometheus(prometheusMux, notifications.prometheusNotificationChannel)
|
|
go func() {
|
|
log.Fatal(http.ListenAndServe(":8001", prometheusMux))
|
|
}()
|
|
admin := matchmaker.NewMatchMaker(notifications)
|
|
|
|
registrationService, clientService, sessionService := setupWebSockets(admin, websessions)
|
|
|
|
context := HttpContext{mux: http.NewServeMux(), path: contextpath}
|
|
|
|
if pprof {
|
|
registerPprof(context.mux, "/debug/pprof")
|
|
}
|
|
setupWebUI(context, registrationService, clientService, sessionService, staticdir, downloaddir)
|
|
// Catch all for the web UI
|
|
context.mux.HandleFunc("/", catchAllHandler(contextpath))
|
|
|
|
// Start HTTP server
|
|
fmt.Println("Rendez-vous server listening on :8000")
|
|
log.Fatal(http.ListenAndServe(":8000", context.mux))
|
|
}
|
|
|
|
func setupWebUI(context HttpContext, registrationService websocketutil.WebSocketService, clientService websocketutil.WebSocketService, sessionService websocketutil.WebSocketService, staticdir string, downloaddir string) HttpContext {
|
|
// The web UI
|
|
|
|
context.HandleFunc("agent/", registrationService.Handle)
|
|
context.HandleFunc("client/", clientService.Handle)
|
|
context.HandleFunc("ws/sessions", sessionService.Handle)
|
|
|
|
// create filehandler with templating for html files.
|
|
context.Handle("docs/", http.StripPrefix("docs/", http.HandlerFunc(pageHandler)))
|
|
context.Handle("static/", http.StripPrefix("static/",
|
|
http.FileServer(http.Dir(staticdir))))
|
|
context.Handle("downloads/", http.StripPrefix("downloads/",
|
|
http.FileServer(http.Dir(downloaddir))))
|
|
// create usage generator
|
|
context.HandleFunc("usage", generateCLIExammple)
|
|
|
|
return context
|
|
}
|
|
|
|
func setupWebSockets(admin *matchmaker.MatchMaker, websessions *matchmaker.WebSessions) (websocketutil.WebSocketService, websocketutil.WebSocketService, websocketutil.WebSocketService) {
|
|
// websocket endpoints
|
|
|
|
// For agents connecting
|
|
registrationService := websocketutil.WebSocketService{
|
|
Handler: func(w http.ResponseWriter, r *http.Request, conn net.Conn) {
|
|
publicId, err := parsePublicId(r.URL.Path)
|
|
if err != nil {
|
|
log.Printf("Cannot parse public id from url: '%v'\n", err)
|
|
return
|
|
}
|
|
log.Printf("Got registration connection: '%s'\n", publicId)
|
|
err = admin.Register(publicId, conn)
|
|
if err != nil {
|
|
log.Printf("Error %v\n", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
// For users connecting with ssh
|
|
clientService := websocketutil.WebSocketService{
|
|
Handler: func(w http.ResponseWriter, r *http.Request, conn net.Conn) {
|
|
publicId, err := parsePublicId(r.URL.Path)
|
|
if err != nil {
|
|
log.Printf("Cannot parse public id from url: '%v'\n", err)
|
|
return
|
|
}
|
|
_, wsProxyMode := r.URL.Query()["wsproxy"]
|
|
log.Printf("Got client connection: '%s'\n", publicId)
|
|
err = admin.Connect(wsProxyMode, publicId, conn)
|
|
if err != nil {
|
|
log.Printf("Error %v\n", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
// for the web browser getting live status updates.
|
|
sessionService := websocketutil.WebSocketService{
|
|
Handler: func(w http.ResponseWriter, r *http.Request, conn net.Conn) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
websession := websessions.NewSession(conn, ctx, cancel)
|
|
defer websessions.SessionClosed(websession)
|
|
location, err := matchmaker.GetUserLocation(r)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
websession.WriteNotifications(location, ctx, cancel)
|
|
},
|
|
Text: true,
|
|
}
|
|
return registrationService, clientService, sessionService
|
|
}
|