internal/home: Add a status page on home

This commit is contained in:
Tom Wiesing 2022-10-06 15:32:02 +02:00
parent 7cda92b342
commit 3d4db1744b
No known key found for this signature in database
9 changed files with 301 additions and 118 deletions

View file

@ -0,0 +1,72 @@
package home
import (
"context"
"fmt"
"net/http"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/component"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/tkw1536/goprogram/stream"
)
type Home struct {
component.ComponentBase
Instances *instances.Instances
RefreshInterval time.Duration
redirect lazy.Lazy[*Redirect]
instanceNames lazy.Lazy[map[string]struct{}]
homeBytes lazy.Lazy[[]byte]
}
func (*Home) Name() string { return "home" }
func (*Home) Routes() []string { return []string{"/"} }
func (home *Home) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
home.updateRedirect(context, io)
home.updateInstances(context, io)
home.updateRender(context, io)
return home, nil
}
func (home *Home) ServeHTTP(w http.ResponseWriter, r *http.Request) {
slug, ok := home.Config.SlugFromHost(r.Host)
switch {
case !ok:
http.NotFound(w, r)
case slug != "":
home.serveWissKI(w, slug, r)
default:
home.serveRoot(w, r)
}
}
func (home *Home) serveRoot(w http.ResponseWriter, r *http.Request) {
// not the root url => server the fallback
if !(r.URL.Path == "" || r.URL.Path == "/") {
home.redirect.Get(nil).ServeHTTP(w, r)
return
}
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusAccepted)
w.Write(home.homeBytes.Get(nil))
}
func (home *Home) serveWissKI(w http.ResponseWriter, slug string, r *http.Request) {
if _, ok := home.instanceNames.Get(nil)[slug]; !ok {
// Get(nil) guaranteed to work by precondition
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "WissKI %q not found\n", slug)
return
}
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintf(w, "WissKI %q is currently offline\n", slug)
}

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<title>WissKI Distillery</title>
<h1>WissKI Distillery</h1>
<p>
For more information, see <a href="{{ .SelfRedirect }}">{{ .SelfRedirect }}</a>.
</p>
<h2>WissKIs on this Distillery</h2>
<div>
{{range .Instances}}
{{ if .Running }}
<h3>{{.Slug}}</h3>
<p>
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a><br />
</p>
{{ end }}
{{ end }}
</div>
<hr />
<footer>
Generated at <code>{{ .Time }}</code>
</footer>

View file

@ -0,0 +1,90 @@
package home
import (
"bytes"
"context"
"text/template"
"time"
_ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
"github.com/FAU-CDI/wisski-distillery/pkg/timex"
"github.com/tkw1536/goprogram/stream"
"golang.org/x/sync/errgroup"
)
func (home *Home) updateInstances(ctx context.Context, io stream.IOStream) {
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
io.Printf("[%s]: reloading instance list", t.String())
names, _ := home.instanceMap()
home.instanceNames.Set(names)
})
}
func (home *Home) instanceMap() (map[string]struct{}, error) {
wissKIs, err := home.Instances.All()
if err != nil {
return nil, err
}
names := make(map[string]struct{}, len(wissKIs))
for _, w := range wissKIs {
names[w.Slug] = struct{}{}
}
return names, nil
}
func (home *Home) updateRender(ctx context.Context, io stream.IOStream) {
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
io.Printf("[%s]: reloading home render", t.String())
bytes, _ := home.homeRender()
home.homeBytes.Set(bytes)
})
}
//go:embed "home.html"
var homeHTMLStr string
var homeTemplate = template.Must(template.New("home.html").Parse(homeHTMLStr))
func (home *Home) homeRender() ([]byte, error) {
var context HomeContext
// setup a couple of static things
context.Time = time.Now().UTC()
context.SelfRedirect = home.Config.SelfRedirect.String()
// find all the WissKIs
wissKIs, err := home.Instances.All()
if err != nil {
return nil, err
}
context.Instances = make([]instances.WissKIInfo, len(wissKIs))
// determine their infos
var eg errgroup.Group
for i, instance := range wissKIs {
i := i
wissKI := instance
eg.Go(func() (err error) {
context.Instances[i], err = wissKI.Info(true)
return
})
}
eg.Wait()
// render the template
var buffer bytes.Buffer
homeTemplate.Execute(&buffer, context)
return buffer.Bytes(), nil
}
type HomeContext struct {
Instances []instances.WissKIInfo
Time time.Time
SelfRedirect string
}

View file

@ -1,36 +1,35 @@
package control
package home
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/component"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
"github.com/FAU-CDI/wisski-distillery/pkg/timex"
"github.com/tkw1536/goprogram/stream"
)
// SelfHandler implements serving the '/' route
type SelfHandler struct {
component.ComponentBase
Instances *instances.Instances
func (home *Home) updateRedirect(ctx context.Context, io stream.IOStream) {
timex.SetInterval(ctx, home.RefreshInterval, func(t time.Time) {
io.Printf("[%s]: reloading overrides", t.String())
redirect, _ := home.loadRedirect()
home.redirect.Set(&redirect)
})
}
func (SelfHandler) Name() string { return "control-self" }
func (home *Home) loadRedirect() (redirect Redirect, err error) {
if redirect.Overrides == nil {
redirect.Overrides = make(map[string]string)
}
redirect.Overrides[""] = home.Config.SelfRedirect.String()
func (*SelfHandler) Routes() []string { return []string{"/"} }
redirect.Absolute = false
redirect.Permanent = false
func (sh *SelfHandler) Handler(route string, context context.Context, io stream.IOStream) (http.Handler, error) {
// create a redirect
var redirect Redirect
var err error
// open the overrides file
overrides, err := sh.Environment.Open(sh.Config.SelfOverridesFile)
io.Printf("loading overrides from %q\n", sh.Config.SelfOverridesFile)
// load the overrides file
overrides, err := home.Environment.Open(home.Config.SelfOverridesFile)
if err != nil {
return redirect, err
}
@ -38,52 +37,13 @@ func (sh *SelfHandler) Handler(route string, context context.Context, io stream.
// decode the overrides file
if err := json.NewDecoder(overrides).Decode(&redirect.Overrides); err != nil {
return nil, err
return redirect, err
}
if redirect.Overrides == nil {
redirect.Overrides = make(map[string]string)
}
redirect.Overrides[""] = sh.Config.SelfRedirect.String()
// create a redirect server
redirect.Fallback, err = sh.selfFallback()
if err != nil {
return nil, err
}
redirect.Absolute = false
redirect.Permanent = false
// and return!
return redirect, nil
}
func (sh *SelfHandler) selfFallback() (http.Handler, error) {
return http.HandlerFunc(sh.serveFallback), nil
}
var notFoundText = []byte("not found")
func (sh *SelfHandler) serveFallback(w http.ResponseWriter, r *http.Request) {
slug := sh.Config.SlugFromHost(r.Host)
if slug == "" {
w.WriteHeader(http.StatusNotFound)
w.Write(notFoundText)
return
}
if ok, _ := sh.Instances.Has(slug); !ok {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "WissKI %q not found\n", slug)
return
}
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintf(w, "WissKI %q is currently offline\n", slug)
}
// Redirect implements a redirect server that redirects all requests.
// It implements http.Handler.
type Redirect struct {

View file

@ -0,0 +1,50 @@
package resolver
import (
"context"
"time"
"github.com/FAU-CDI/wisski-distillery/pkg/timex"
"github.com/tkw1536/goprogram/stream"
)
// updatePrefixes starts updating prefixes
func (resolver *Resolver) updatePrefixes(io stream.IOStream, ctx context.Context) {
timex.SetInterval(ctx, resolver.RefreshInterval, func(t time.Time) {
io.Printf("[%s]: reloading prefixes", t.String())
prefixes, _ := resolver.AllPrefixes()
resolver.prefixes.Set(prefixes)
})
}
// AllPrefixes returns a list of all prefixes from the server.
// Prefixes may be cached on the server
func (resolver *Resolver) AllPrefixes() (map[string]string, error) {
instances, err := resolver.Instances.All()
if err != nil {
return nil, err
}
gPrefixes := make(map[string]string)
var lastErr error
for _, instance := range instances {
if instance.NoPrefix() {
continue
}
url := instance.URL().String()
// failed to fetch prefixes for this particular instance
// => skip it!
prefixes, err := instance.PrefixesCached()
if err != nil {
lastErr = err
continue
}
for _, p := range prefixes {
gPrefixes[p] = url
}
}
return gPrefixes, lastErr
}

View file

@ -52,7 +52,8 @@ func (resolver *Resolver) Handler(route string, context context.Context, io stre
io.Printf("registering legacy domain %s\n", domain)
}
go resolver.updatePrefixes(io, context)
// start updating prefixes
resolver.updatePrefixes(io, context)
// resolve the prefixes
p.Resolver = resolvers.InOrder{
@ -63,62 +64,11 @@ func (resolver *Resolver) Handler(route string, context context.Context, io stre
}), err
}
func (resolver *Resolver) updatePrefixes(io stream.IOStream, ctx context.Context) {
t := time.NewTicker(resolver.RefreshInterval)
defer t.Stop()
for {
select {
case <-t.C:
io.Println("resolver: Reloading prefixes from database")
prefixes, _ := resolver.AllPrefixes()
resolver.prefixes.Set(prefixes)
case <-ctx.Done():
return
}
}
}
func (resolver *Resolver) Target(uri string) string {
return wdresolve.PrefixTarget(resolver, uri)
}
// Prefixes returns a cached list of prefixes
func (resolver *Resolver) Prefixes() (prefixes map[string]string) {
return resolver.prefixes.Get(func() map[string]string {
prefixes, _ := resolver.AllPrefixes()
return prefixes
})
}
// AllPrefixes returns a list of all prefixes from the server.
// Prefixes may be cached on the server
func (resolver *Resolver) AllPrefixes() (map[string]string, error) {
instances, err := resolver.Instances.All()
if err != nil {
return nil, err
}
gPrefixes := make(map[string]string)
var lastErr error
for _, instance := range instances {
if instance.NoPrefix() {
continue
}
url := instance.URL().String()
// failed to fetch prefixes for this particular instance
// => skip it!
prefixes, err := instance.PrefixesCached()
if err != nil {
lastErr = err
continue
}
for _, p := range prefixes {
gPrefixes[p] = url
}
}
return gPrefixes, lastErr
return resolver.prefixes.Get(nil) // by precondition there always is a cached value
}

View file

@ -42,18 +42,26 @@ func (cfg Config) DefaultSSLHost() string {
return cfg.IfHttps(cfg.DefaultHost())
}
// SlugFromHost returns the slug belonging to the appropriate host.
func (cfg Config) SlugFromHost(host string) (slug string) {
// SlugFromHost returns the slug belonging to the appropriate host.'
//
// When host is a top-level domain, returns "", true.
// When no slug is found, returns "", false.
func (cfg Config) SlugFromHost(host string) (slug string, ok bool) {
// extract an ':port' that happens to be in the host.
domain, _, _ := strings.Cut(host, ":")
domainL := strings.ToLower(domain)
// check all the possible domain endings
for _, suffix := range append([]string{cfg.DefaultDomain}, cfg.SelfExtraDomains...) {
if strings.HasSuffix(domain, "."+suffix) {
return domain[:len(domain)-len(suffix)-1]
suffixL := strings.ToLower(suffix)
if domainL == suffixL {
return "", true
}
if strings.HasSuffix(domainL, "."+suffixL) {
return domain[:len(domain)-len(suffix)-1], true
}
}
// no domain found!
return ""
return "", ok
}

View file

@ -5,6 +5,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/internal/component"
"github.com/FAU-CDI/wisski-distillery/internal/component/control"
"github.com/FAU-CDI/wisski-distillery/internal/component/home"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/component/resolver"
"github.com/FAU-CDI/wisski-distillery/internal/component/snapshots"
@ -43,7 +44,9 @@ func (dis *Distillery) register(context *component.PoolContext) []component.Comp
// Control server
ra[*control.Control](dis, context),
ra[*control.SelfHandler](dis, context),
r(dis, context, func(home *home.Home) {
home.RefreshInterval = time.Minute
}),
r(dis, context, func(resolver *resolver.Resolver) {
resolver.RefreshInterval = time.Minute
}),

25
pkg/timex/timex.go Normal file
View file

@ -0,0 +1,25 @@
package timex
import (
"context"
"time"
)
// SetInterval invokes f with the current time and then spawns a new goroutine that runs f every d, until context is closed.
func SetInterval(ctx context.Context, d time.Duration, f func(t time.Time)) {
f(time.Now())
go func() {
t := time.NewTicker(d)
defer t.Stop()
for {
select {
case tick := <-t.C:
f(tick)
case <-ctx.Done():
return
}
}
}()
}