moved all ui stuff to the ui package.

The structure of converge server is now much more clear in the package
// structure below pkg/server.
This commit is contained in:
Erik Brakkee 2024-08-17 10:37:20 +02:00
parent 2b9821f967
commit 2c42f89547
10 changed files with 73 additions and 55 deletions

View File

@ -6,6 +6,7 @@ import (
"git.wamblee.org/converge/pkg/models" "git.wamblee.org/converge/pkg/models"
"git.wamblee.org/converge/pkg/server/matchmaker" "git.wamblee.org/converge/pkg/server/matchmaker"
"git.wamblee.org/converge/pkg/server/prometheus" "git.wamblee.org/converge/pkg/server/prometheus"
"git.wamblee.org/converge/pkg/server/ui"
"git.wamblee.org/converge/pkg/support/websocketutil" "git.wamblee.org/converge/pkg/support/websocketutil"
"log" "log"
"net" "net"
@ -128,7 +129,7 @@ func main() {
// And the MatchMaker. The MatchMakers sends state notifications to websessions // And the MatchMaker. The MatchMakers sends state notifications to websessions
// and prometheus. // and prometheus.
notifications := NewStateNotifier(throttlingInterval) notifications := NewStateNotifier(throttlingInterval)
websessions := matchmaker.NewWebSessions(notifications.webNotificationChannel) websessions := ui.NewWebSessions(notifications.webNotificationChannel)
// monitoring // monitoring
prometheusMux := http.NewServeMux() prometheusMux := http.NewServeMux()
prometheus.SetupPrometheus(prometheusMux, notifications.prometheusNotificationChannel) prometheus.SetupPrometheus(prometheusMux, notifications.prometheusNotificationChannel)
@ -161,18 +162,18 @@ func setupWebUI(context HttpContext, registrationService websocketutil.WebSocket
context.HandleFunc("ws/sessions", sessionService.Handle) context.HandleFunc("ws/sessions", sessionService.Handle)
// create filehandler with templating for html files. // create filehandler with templating for html files.
context.Handle("docs/", http.StripPrefix("docs/", http.HandlerFunc(pageHandler))) context.Handle("docs/", http.StripPrefix("docs/", http.HandlerFunc(ui.PageHandler)))
context.Handle("static/", http.StripPrefix("static/", context.Handle("static/", http.StripPrefix("static/",
http.FileServer(http.Dir(staticdir)))) http.FileServer(http.Dir(staticdir))))
context.Handle("downloads/", http.StripPrefix("downloads/", context.Handle("downloads/", http.StripPrefix("downloads/",
http.FileServer(http.Dir(downloaddir)))) http.FileServer(http.Dir(downloaddir))))
// create usage generator // create usage generator
context.HandleFunc("usage", generateCLIExammple) context.HandleFunc("usage", ui.GenerateCLIExammples)
return context return context
} }
func setupWebSockets(admin *matchmaker.MatchMaker, websessions *matchmaker.WebSessions) (websocketutil.WebSocketService, websocketutil.WebSocketService, websocketutil.WebSocketService) { func setupWebSockets(admin *matchmaker.MatchMaker, websessions *ui.WebSessions) (websocketutil.WebSocketService, websocketutil.WebSocketService, websocketutil.WebSocketService) {
// websocket endpoints // websocket endpoints
// For agents connecting // For agents connecting
@ -214,7 +215,7 @@ func setupWebSockets(admin *matchmaker.MatchMaker, websessions *matchmaker.WebSe
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
websession := websessions.NewSession(conn, ctx, cancel) websession := websessions.NewSession(conn, ctx, cancel)
defer websessions.SessionClosed(websession) defer websessions.SessionClosed(websession)
location, err := matchmaker.GetUserLocation(r) location, err := ui.GetUserLocation(r)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -1,28 +0,0 @@
package main
import (
templates2 "git.wamblee.org/converge/pkg/server/ui"
"net/http"
)
func pageHandler(w http.ResponseWriter, r *http.Request) {
access := getConvergeAccess(r)
switch r.URL.Path {
case "":
fallthrough
case "/":
fallthrough
case "index.html":
templates2.AboutTab().Render(r.Context(), w)
// TODO use contexts later.
case "usage.html":
templates2.UsageTab(access).Render(r.Context(), w)
case "downloads.html":
templates2.DownloadsTab().Render(r.Context(), w)
case "sessions.html":
templates2.SessionsTab(nil, nil, access.Location).Render(r.Context(), w)
default:
http.NotFound(w, r)
}
}

4
pkg/server/admin/doc.go Normal file
View File

@ -0,0 +1,4 @@
// Basic administration of the matchmaker.
// Put in its separate package to make sure only public methods can be called because
// these are the only ones that are thread-safe.
package admin

View File

@ -0,0 +1,6 @@
// The matchmaker package matches up agents and clients based on their rendez-vous
// ids and pumps data between client and agent. This is the core cunctionality of
// converge. It uses notifications with the new state in case something changes.
// The notifications are routed to components that are interested in them: the web ui
// and prometheus integration.
package matchmaker

View File

@ -0,0 +1,4 @@
// Prometheus integration of converge. It relies on state notifications of converge that
// describe the current state. It computes the deltas based on the state itself and updates
// prometheus metrics accordingly.
package prometheus

View File

@ -1,13 +1,24 @@
package main package ui
import ( import (
"git.wamblee.org/converge/pkg/models" "git.wamblee.org/converge/pkg/models"
"git.wamblee.org/converge/pkg/server/matchmaker"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
"time"
) )
func GetUserLocation(r *http.Request) (*time.Location, error) {
tzName := r.URL.Query().Get("timezone")
if tzName == "" {
tzName = r.Header.Get("X-Timezone")
}
if tzName == "" {
tzName = "UTC"
}
return time.LoadLocation(tzName)
}
func getConvergeAccess(r *http.Request) models.ConvergeAccess { func getConvergeAccess(r *http.Request) models.ConvergeAccess {
pattern := regexp.MustCompile("^(.*)/usage$") pattern := regexp.MustCompile("^(.*)/usage$")
@ -32,7 +43,7 @@ func getConvergeAccess(r *http.Request) models.ConvergeAccess {
} }
} }
location, err := matchmaker.GetUserLocation(r) location, err := GetUserLocation(r)
if err != nil { if err != nil {
panic(err) panic(err)
} }

6
pkg/server/ui/doc.go Normal file
View File

@ -0,0 +1,6 @@
// This provides the user interface consisting of a number of an information page,
// a downloads page, an interfactive usage page and a sessions page.
// The usage page uses HTMX to get updated CLI examples based on the user's input.
// The sessions page uses HTMX with a websocket so that the server can push the live
// status of the server to clients.
package ui

View File

@ -0,0 +1,27 @@
package ui
import (
"net/http"
)
func PageHandler(w http.ResponseWriter, r *http.Request) {
access := getConvergeAccess(r)
switch r.URL.Path {
case "":
fallthrough
case "/":
fallthrough
case "index.html":
AboutTab().Render(r.Context(), w)
// TODO use contexts later.
case "usage.html":
UsageTab(access).Render(r.Context(), w)
case "downloads.html":
DownloadsTab().Render(r.Context(), w)
case "sessions.html":
SessionsTab(nil, nil, access.Location).Render(r.Context(), w)
default:
http.NotFound(w, r)
}
}

View File

@ -1,8 +1,7 @@
package main package ui
import ( import (
"fmt" "fmt"
"git.wamblee.org/converge/pkg/server/ui"
"github.com/gliderlabs/ssh" "github.com/gliderlabs/ssh"
"math/rand" "math/rand"
"net/http" "net/http"
@ -11,7 +10,7 @@ import (
"strings" "strings"
) )
func generateCLIExammple(w http.ResponseWriter, r *http.Request) { func GenerateCLIExammples(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest) http.Error(w, "Error parsing form", http.StatusBadRequest)
@ -32,7 +31,7 @@ func generateCLIExammple(w http.ResponseWriter, r *http.Request) {
downloadCommand := r.FormValue("download-command") downloadCommand := r.FormValue("download-command")
certificateValidation := r.FormValue("certificate-validation") != "" certificateValidation := r.FormValue("certificate-validation") != ""
sshPublicKeys := strings.Split(keysString, "\n") sshPublicKeys := strings.Split(keysString, "\n")
usageInputs := ui.NewUsageInputs(id, sshPublicKeys, remoteShells, localShells, downloadCommand, usageInputs := NewUsageInputs(id, sshPublicKeys, remoteShells, localShells, downloadCommand,
certificateValidation) certificateValidation)
if generatedId { if generatedId {
usageInputs.ErrorMessages = append(usageInputs.ErrorMessages, "rendez-vous id is randomly generated, changes on every page refresh") usageInputs.ErrorMessages = append(usageInputs.ErrorMessages, "rendez-vous id is randomly generated, changes on every page refresh")
@ -64,7 +63,7 @@ func generateCLIExammple(w http.ResponseWriter, r *http.Request) {
"No valid public keys configured. Without these the agent will not work.") "No valid public keys configured. Without these the agent will not work.")
} }
err = ui.ShellUsage(access, usageInputs).Render(r.Context(), w) err = ShellUsage(access, usageInputs).Render(r.Context(), w)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
} }

View File

@ -1,9 +1,8 @@
package matchmaker package ui
import ( import (
"context" "context"
"git.wamblee.org/converge/pkg/models" "git.wamblee.org/converge/pkg/models"
"git.wamblee.org/converge/pkg/server/ui"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -98,17 +97,6 @@ func (sessions *WebSessions) NewSession(wsConnection net.Conn, ctx context.Conte
return session return session
} }
func GetUserLocation(r *http.Request) (*time.Location, error) {
tzName := r.URL.Query().Get("timezone")
if tzName == "" {
tzName = r.Header.Get("X-Timezone")
}
if tzName == "" {
tzName = "UTC"
}
return time.LoadLocation(tzName)
}
func (session *WebSession) WriteNotifications(location *time.Location, ctx context.Context, cancel context.CancelFunc) { func (session *WebSession) WriteNotifications(location *time.Location, ctx context.Context, cancel context.CancelFunc) {
timer := time.NewTicker(10 * time.Second) timer := time.NewTicker(10 * time.Second)
defer timer.Stop() defer timer.Stop()
@ -138,7 +126,7 @@ func (session *WebSession) WriteNotifications(location *time.Location, ctx conte
func (session *WebSession) writeNotificationToClient(location *time.Location, notification *models.State) bool { func (session *WebSession) writeNotificationToClient(location *time.Location, notification *models.State) bool {
agents, clients := notification.Slices() agents, clients := notification.Slices()
err := ui.State(agents, clients, location).Render(context.Background(), session.conn) err := State(agents, clients, location).Render(context.Background(), session.conn)
if err != nil { if err != nil {
log.Printf("WS connection closed: %v", err) log.Printf("WS connection closed: %v", err)
return false return false