internal/home: Add a status page on home
This commit is contained in:
parent
7cda92b342
commit
3d4db1744b
9 changed files with 301 additions and 118 deletions
72
internal/component/home/home.go
Normal file
72
internal/component/home/home.go
Normal 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)
|
||||
}
|
||||
25
internal/component/home/home.html
Normal file
25
internal/component/home/home.html
Normal 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>
|
||||
90
internal/component/home/public.go
Normal file
90
internal/component/home/public.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
50
internal/component/resolver/prefixes.go
Normal file
50
internal/component/resolver/prefixes.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
25
pkg/timex/timex.go
Normal 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
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue