From 117f5a05a15a7d1cab1a63fd0098bdd2a4fa2732 Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 29 Feb 2024 17:17:21 +0100 Subject: [PATCH] feat(agent): serve status page --- internal/agent/agent.go | 1 + internal/agent/context.go | 16 +- .../controller/registration/controller.go | 143 ++++++++++++++++++ .../agent/controller/registration/handler.go | 74 +++++++++ .../controller/registration/public/style.css | 0 .../registration/templates/index.html.gotpl | 14 ++ internal/command/agent/run.go | 7 + internal/config/agent.go | 24 ++- pkg/client/client.go | 4 + 9 files changed, 275 insertions(+), 8 deletions(-) create mode 100644 internal/agent/controller/registration/controller.go create mode 100644 internal/agent/controller/registration/handler.go create mode 100644 internal/agent/controller/registration/public/style.css create mode 100644 internal/agent/controller/registration/templates/index.html.gotpl diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 7949d01..c4aa1be 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -38,6 +38,7 @@ func (a *Agent) Run(ctx context.Context) error { client := client.New(a.serverURL, client.WithToken(token)) ctx = withClient(ctx, client) + ctx = withThumbprint(ctx, a.thumbprint) tick := func() { logger.Debug(ctx, "registering agent") diff --git a/internal/agent/context.go b/internal/agent/context.go index 83a80d1..9f45675 100644 --- a/internal/agent/context.go +++ b/internal/agent/context.go @@ -10,7 +10,8 @@ import ( type contextKey string const ( - contextKeyClient contextKey = "client" + contextKeyClient contextKey = "client" + contextKeyThumbprint contextKey = "thumbprint" ) func withClient(ctx context.Context, client *client.Client) context.Context { @@ -25,3 +26,16 @@ func Client(ctx context.Context) *client.Client { return client } + +func withThumbprint(ctx context.Context, thumbprint string) context.Context { + return context.WithValue(ctx, contextKeyThumbprint, thumbprint) +} + +func Thumbprint(ctx context.Context) string { + thumbprint, ok := ctx.Value(contextKeyThumbprint).(string) + if !ok { + panic(errors.New("could not retrieve thumbprint from context")) + } + + return thumbprint +} diff --git a/internal/agent/controller/registration/controller.go b/internal/agent/controller/registration/controller.go new file mode 100644 index 0000000..5dac774 --- /dev/null +++ b/internal/agent/controller/registration/controller.go @@ -0,0 +1,143 @@ +package registration + +import ( + "context" + "net/http" + "sync/atomic" + + "forge.cadoles.com/Cadoles/emissary/internal/agent" + "forge.cadoles.com/Cadoles/emissary/internal/datastore" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type Status struct { + Agent *datastore.Agent + Connected bool + Claimed bool + Thumbprint string + ServerURL string +} + +type Controller struct { + status *atomic.Value + server *atomic.Value + addr string +} + +// Name implements node.Controller. +func (c *Controller) Name() string { + return "registration-controller" +} + +// Reconcile implements node.Controller. +func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error { + cl := agent.Client(ctx) + thumbprint := agent.Thumbprint(ctx) + + connected := true + agent, err := cl.GetAgent( + ctx, + state.AgentID(), + ) + if err != nil { + logger.Error(ctx, "could not get agent", logger.E(errors.WithStack(err))) + connected = false + } + + claimed := agent != nil && agent.TenantID != nil + + c.status.Store(Status{ + Agent: agent, + Connected: connected, + Claimed: claimed, + Thumbprint: thumbprint, + ServerURL: cl.ServerURL(), + }) + + if err := c.reconcileAgent(ctx, connected, claimed); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (c *Controller) reconcileAgent(ctx context.Context, connected bool, claimed bool) error { + shouldStart := !connected || !claimed + + if shouldStart { + if err := c.startServer(ctx); err != nil { + return errors.WithStack(err) + } + } else { + if err := c.stopServer(ctx); err != nil { + return errors.WithStack(err) + } + } + + return nil +} + +func (c *Controller) startServer(ctx context.Context) error { + server := c.getServer() + if server != nil { + return nil + } + + server = &http.Server{ + Addr: c.addr, + Handler: &Handler{ + status: c.status, + }, + } + + go func() { + defer c.setServer(nil) + + if err := server.ListenAndServe(); err != nil { + logger.Error(ctx, "could not start server", logger.E(errors.WithStack(err))) + } + }() + + c.setServer(server) + + return nil +} + +func (c *Controller) stopServer(ctx context.Context) error { + server := c.getServer() + if server == nil { + return nil + } + + defer c.setServer(nil) + + if err := server.Close(); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func (c *Controller) setServer(s *http.Server) { + c.server.Store(s) +} + +func (c *Controller) getServer() *http.Server { + server, ok := c.server.Load().(*http.Server) + if !ok { + return nil + } + + return server +} + +func NewController(addr string) *Controller { + return &Controller{ + addr: addr, + status: &atomic.Value{}, + server: &atomic.Value{}, + } +} + +var _ agent.Controller = &Controller{} diff --git a/internal/agent/controller/registration/handler.go b/internal/agent/controller/registration/handler.go new file mode 100644 index 0000000..9a7c2fc --- /dev/null +++ b/internal/agent/controller/registration/handler.go @@ -0,0 +1,74 @@ +package registration + +import ( + "embed" + "html/template" + "io/fs" + "net/http" + "sync" + "sync/atomic" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +//go:embed templates/*.gotpl +var templates embed.FS + +//go:embed public/* +var public embed.FS + +type Handler struct { + status *atomic.Value + + public http.Handler + templates *template.Template + + init sync.Once + initErr error +} + +// ServeHTTP implements http.Handler. +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.init.Do(func() { + root, err := fs.Sub(public, "public") + if err != nil { + h.initErr = errors.WithStack(err) + return + } + + h.public = http.FileServer(http.FS(root)) + + tmpl, err := template.ParseFS(templates, "templates/*.gotpl") + if err != nil { + h.initErr = errors.WithStack(err) + return + } + + h.templates = tmpl + }) + if h.initErr != nil { + logger.Error(r.Context(), "could not initialize handler", logger.E(h.initErr)) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + + return + } + + switch r.URL.Path { + case "/": + h.serveIndex(w, r) + default: + h.public.ServeHTTP(w, r) + } +} + +func (h *Handler) serveIndex(w http.ResponseWriter, r *http.Request) { + data := h.status.Load() + + if err := h.templates.ExecuteTemplate(w, "index.html.gotpl", data); err != nil { + logger.Error(r.Context(), "could not render template", logger.E(errors.WithStack(err))) + return + } +} + +var _ http.Handler = &Handler{} diff --git a/internal/agent/controller/registration/public/style.css b/internal/agent/controller/registration/public/style.css new file mode 100644 index 0000000..e69de29 diff --git a/internal/agent/controller/registration/templates/index.html.gotpl b/internal/agent/controller/registration/templates/index.html.gotpl new file mode 100644 index 0000000..c454061 --- /dev/null +++ b/internal/agent/controller/registration/templates/index.html.gotpl @@ -0,0 +1,14 @@ + + + + + +

Emissary

+ + + \ No newline at end of file diff --git a/internal/command/agent/run.go b/internal/command/agent/run.go index 0c2f782..bec86d6 100644 --- a/internal/command/agent/run.go +++ b/internal/command/agent/run.go @@ -9,6 +9,7 @@ import ( "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/proxy" + "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/registration" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/spec" "forge.cadoles.com/Cadoles/emissary/internal/agent/metadata" "forge.cadoles.com/Cadoles/emissary/internal/agent/metadata/collector/buildinfo" @@ -94,6 +95,12 @@ func RunCommand() *cli.Command { )) } + if ctrlConf.Registration.Enabled { + controllers = append(controllers, registration.NewController( + string(ctrlConf.Registration.Address), + )) + } + key, err := jwk.LoadOrGenerate(string(conf.Agent.PrivateKeyPath), jwk.DefaultKeySize) if err != nil { return errors.WithStack(err) diff --git a/internal/config/agent.go b/internal/config/agent.go index 0e582df..6af34b3 100644 --- a/internal/config/agent.go +++ b/internal/config/agent.go @@ -17,13 +17,14 @@ type ShellCollectorConfig struct { } type ControllersConfig struct { - Persistence PersistenceControllerConfig `yaml:"persistence"` - Spec SpecControllerConfig `yaml:"spec"` - Proxy ProxyControllerConfig `yaml:"proxy"` - UCI UCIControllerConfig `yaml:"uci"` - App AppControllerConfig `yaml:"app"` - SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"` - MDNS MDNSControllerConfig `yaml:"mdns"` + Persistence PersistenceControllerConfig `yaml:"persistence"` + Spec SpecControllerConfig `yaml:"spec"` + Proxy ProxyControllerConfig `yaml:"proxy"` + UCI UCIControllerConfig `yaml:"uci"` + App AppControllerConfig `yaml:"app"` + SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"` + MDNS MDNSControllerConfig `yaml:"mdns"` + Registration RegistrationControllerConfig `yaml:"registration"` } type PersistenceControllerConfig struct { @@ -60,6 +61,11 @@ type MDNSControllerConfig struct { Enabled InterpolatedBool `yaml:"enabled"` } +type RegistrationControllerConfig struct { + Enabled InterpolatedBool `yaml:"enabled"` + Address InterpolatedString `yaml:"address"` +} + func NewDefaultAgentConfig() AgentConfig { return AgentConfig{ ServerURL: "http://127.0.0.1:3000", @@ -94,6 +100,10 @@ func NewDefaultAgentConfig() AgentConfig { MDNS: MDNSControllerConfig{ Enabled: true, }, + Registration: RegistrationControllerConfig{ + Enabled: true, + Address: ":42521", + }, }, Collectors: []ShellCollectorConfig{ { diff --git a/pkg/client/client.go b/pkg/client/client.go index 8c8d2fb..c47b526 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -18,6 +18,10 @@ type Client struct { serverURL string } +func (c *Client) ServerURL() string { + return c.serverURL +} + func (c *Client) apiGet(ctx context.Context, path string, result any, funcs ...OptionFunc) error { if err := c.apiDo(ctx, http.MethodGet, path, nil, result, funcs...); err != nil { return errors.WithStack(err)