Initial status page

This commit is contained in:
Tom Wiesing 2022-09-16 17:54:40 +02:00
parent a3511b1bfc
commit a1f35b97d3
No known key found for this signature in database
17 changed files with 618 additions and 83 deletions

View file

@ -0,0 +1,55 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/dis/static/dis.css">
<link rel="stylesheet" href="/dis/static/autolink.css">
<title>Distillery Status Page</title>
<h1 id="top">Distillery Status Page</h1>
<h2 id="overview">Overview</h2>
<p>
<b>Domain:</b> <code>{{.Config.DefaultDomain}}</code> <br />
<b>Legacy Domain(s):</b> <code>{{.Config.SelfExtraDomains}}</code><br />
<b>HTTPS Email:</b> <code>{{.Config.CertbotEmail}}</code><br />
<hr />
<b>Homepage Redirect:</b><a href="{{.Config.SelfRedirect}}" target="_blank" rel="noopener noreferrer">{{.Config.SelfRedirect}}</a><br />
<hr />
<b>Backup Age:</b> <code>{{.Config.MaxBackupAge}}</code> Day(s)<br />
<hr />
<b>Base Directory:</b> <code>{{.Config.DeployRoot}}</code><br />
<b>Configuration File:</b> <code>{{.Config.ConfigPath}}</code><br />
<b>Authorized_Keys File:</b> <code>{{.Config.GlobalAuthorizedKeysFile}}</code><br />
<hr />
<b>MySQL User Prefix:</b> <code>{{.Config.MysqlUserPrefix}}</code><br />
<b>MySQL Database Prefix:</b> <code>{{.Config.MysqlDatabasePrefix}}</code><br />
<b>GraphDB User Prefix:</b> <code>{{.Config.GraphDBUserPrefix}}</code><br />
<b>GraphDB Database Prefix:</b> <code>{{.Config.GraphDBRepoPrefix}}</code><br />
<hr />
<b>Bookkeeping Database:</b> <code>{{.Config.DistilleryBookkeepingDatabase}}</code><br />
<b>Bookkeeping Table:</b> <code>{{.Config.DistilleryBookkeepingTable}}</code><br />
</p>
<h2 id="instances">Instances</h2>
<p>
<code>{{ .TotalCount }}</code> instance(s) = <code>{{ .RunningCount }}</code> running + <code>{{ .StoppedCount }}</code> stopped<br />
</p>
{{range .Instances}}
<div class="wisski {{ if .Running }}running{{ else }}stopped{{ end }}">
<h3 id="instance-{{.Slug}}">{{.Slug}}{{ if not .Running }}&nbsp;<small>not running</small>{{ end }}</h3>
<p>
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a><br />
<small>
<a href="/dis/instance/{{.Slug}}">More Details</a>
</small>
</p>
</div>
{{end}}
<footer>
Generated at <code>{{ .Time }}</code>
</footer>
<script src="/dis/static/autolink.js"></script>

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/dis/static/dis.css">
<title>Distillery Status Page - {{ .Info.Slug }}</title>
<h1 id="top">Distillery Status Page - {{ .Info.Slug }}</h1>
<p>
<a href="/dis/index">Back to index</a>
</p>
<pre>{{ . }}</pre>
<footer>
Generated at <code>{{ .Time }}</code>
</footer>
<script src="/dis/static/autolink.js"></script>

View file

@ -0,0 +1,20 @@
.header-link {
position: relative;
left: 0.5em;
opacity: 0;
font-size: 0.8em;
-webkit-transition: opacity 0.2s ease-in-out 0.1s;
-moz-transition: opacity 0.2s ease-in-out 0.1s;
-ms-transition: opacity 0.2s ease-in-out 0.1s;
text-decoration: none;
}
h2:hover .header-link,
h3:hover .header-link,
h4:hover .header-link,
h5:hover .header-link,
h6:hover .header-link {
opacity: 1;
}

View file

@ -0,0 +1,24 @@
/** adding links to each item, see http://blog.parkermoore.de/2014/08/01/header-anchor-links-in-vanilla-javascript-for-github-pages-and-jekyll/ */
var anchorForId = function (id) {
var anchor = document.createElement("a");
anchor.className = "header-link";
anchor.href = "#" + id;
anchor.innerHTML = "#";
return anchor;
};
var linkifyAnchors = function (level) {
var headers = document.getElementsByTagName("h" + level);
for (var h = 0; h < headers.length; h++) {
var header = headers[h];
if (typeof header.id !== "undefined" && header.id !== "") {
header.appendChild(anchorForId(header.id));
}
}
};
for (var level = 1; level <= 6; level++) {
linkifyAnchors(level);
}

View file

@ -0,0 +1,67 @@
html {
line-height: 1.5;
font-family: Roboto;
font-size: 20px;
color: #1a1a1a;
background-color: #fdfdfd;
}
body {
margin: 0 auto;
max-width: 36em;
padding-left: 50px;
padding-right: 50px;
padding-top: 50px;
padding-bottom: 50px;
hyphens: auto;
overflow-wrap: break-word;
text-rendering: optimizeLegibility;
font-kerning: normal;
}
@media (max-width: 600px) {
body {
font-size: 0.9em;
padding: 1em;
}
}
h1 {
margin-top: 1.4em;
}
h2,h3 {
margin-top: 1em;
}
code {
font-family: Roboto Mono;
color: blue;
}
p {
margin: 1em 0;
text-align: justify;
}
a, a:visited {
color: #1a1a1a;
}
footer {
border-top: 1px solid #1a1a1a;
font-size: small;
text-align: center;
}
.wisski {
padding-left: 5px;
}
.wisski.running {
background-color: green;
}
.wisski.stopped {
background-color: red;
}

View file

@ -1,70 +1,198 @@
package dis
import (
"encoding/json"
"embed"
"html/template"
"io/fs"
"net/http"
"strings"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
"github.com/FAU-CDI/wisski-distillery/internal/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/config"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/tkw1536/goprogram/stream"
"golang.org/x/sync/errgroup"
_ "embed"
)
func (dis *Dis) info(io stream.IOStream) (http.Handler, error) {
return http.HandlerFunc(dis.handleDis), nil
mux := http.NewServeMux()
// handle everything under /dis/!
mux.HandleFunc("/dis/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/dis/" {
http.Redirect(w, r, "/dis/index", http.StatusTemporaryRedirect)
return
}
http.NotFound(w, r)
})
// static stuff
static, err := dis.disStatic()
if err != nil {
return nil, err
}
mux.Handle("/dis/static/", static)
// render everything
mux.Handle("/dis/index", httpx.HTMLHandler[disIndex]{
Handler: dis.disIndex,
Template: indexTemplate,
})
mux.Handle("/dis/instance/", httpx.HTMLHandler[disInstance]{
Handler: dis.disInstance,
Template: instanceTemplate,
})
// api -- for future usage
mux.Handle("/dis/api/v1/instance/get/", httpx.JSON(dis.getinstance))
mux.Handle("/dis/api/v1/instance/all", httpx.JSON(dis.allinstances))
// ensure that everyone is logged in!
return httpx.BasicAuth(mux, "WissKI Distillery Admin", func(user, pass string) bool {
return user == dis.Config.DisAdminUser && pass == dis.Config.DisAdminPassword
}), nil
}
const disLimit = 2
// disIndex is the context of the "/dis/index" page
type disIndex struct {
Time time.Time
func (dis *Dis) handleDis(w http.ResponseWriter, r *http.Request) {
// make sure the user is authorized
if !dis.authDis(r) {
w.Header().Add("WWW-Authenticate", `Basic realm="WissKI Distillery Admin"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
Config *config.Config
Instances []instances.Info
TotalCount int
RunningCount int
StoppedCount int
}
func (dis *Dis) disIndex(r *http.Request) (idx disIndex, err error) {
// load instances
idx.Instances, err = dis.allinstances(r)
if err != nil {
return
}
// create a new error group
// count how many are running and how many are stopped
for _, i := range idx.Instances {
if i.Running {
idx.RunningCount++
} else {
idx.StoppedCount++
}
}
idx.TotalCount = len(idx.Instances)
// get the static properties
idx.Config = dis.Config
// current time
idx.Time = time.Now()
return
}
// disInstance is the context of the "/dis/instance/*" page
type disInstance struct {
Time time.Time
Instance bookkeeping.Instance
Info instances.Info
}
func (dis *Dis) disInstance(r *http.Request) (is disInstance, err error) {
// find the slug as the last component of path!
slug := strings.TrimSuffix(r.URL.Path, "/")
slug = slug[strings.LastIndex(slug, "/")+1:]
// find the instance itself!
instance, err := dis.Instances.WissKI(slug)
if err == instances.ErrWissKINotFound {
return is, httpx.ErrNotFound
}
if err != nil {
return is, err
}
is.Instance = instance.Instance
// get some more info about the wisski
is.Info, err = instance.Info(false)
if err != nil {
return is, err
}
// current time
is.Time = time.Now()
return
}
//go:embed html/static
var htmlStaticFS embed.FS
func (*Dis) disStatic() (http.Handler, error) {
fs, err := fs.Sub(htmlStaticFS, "html/static")
if err != nil {
return nil, err
}
return http.StripPrefix("/dis/static/", http.FileServer(http.FS(fs))), nil
}
//go:embed "html/index.html"
var indexTemplateStr string
var indexTemplate = template.Must(template.New("index.html").Parse(indexTemplateStr))
//go:embed "html/instance.html"
var instanceTemplateString string
var instanceTemplate = template.Must(template.New("instance.html").Parse(instanceTemplateString))
func (dis *Dis) getinstance(r *http.Request) (info instances.Info, err error) {
// find the slug as the last component of path!
slug := strings.TrimSuffix(r.URL.Path, "/")
slug = slug[strings.LastIndex(slug, "/")+1:]
// load the wisski instance!
wisski, err := dis.Instances.WissKI(strings.TrimSuffix(slug, "/"))
if err == instances.ErrWissKINotFound {
return info, httpx.ErrNotFound
}
if err != nil {
return info, err
}
// get info about it!
return wisski.Info(false)
}
func (dis *Dis) allinstances(*http.Request) (infos []instances.Info, err error) {
var errgroup errgroup.Group
errgroup.SetLimit(disLimit)
// list all the instances
all, err := dis.Instances.All()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("internal server error"))
return
return nil, err
}
// get all of their info!
infos := make([]instances.Info, len(all))
infos = make([]instances.Info, len(all))
for i, instance := range all {
{
i := i
instance := instance
errgroup.Go(func() (err error) {
infos[i], err = instance.Info()
infos[i], err = instance.Info(true)
return err
})
}
}
// if some info call failed
if err := errgroup.Wait(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("internal server error"))
w.Write([]byte("\n"))
return
}
// and return the json
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(infos)
}
func (dis *Dis) authDis(r *http.Request) bool {
user, pass, ok := r.BasicAuth()
return ok && user == dis.Config.DisAdminUser && pass == dis.Config.DisAdminPassword
// wait for the results, and return
err = errgroup.Wait()
return
}

View file

@ -2,4 +2,4 @@ FROM docker.io/library/docker:20.10-cli
COPY wdcli /wdcli
EXPOSE 8888
CMD ["/wdcli","--internal-in-docker","--config","${CONFIG_PATH}","server","--bind","0.0.0.0:8888"]
CMD ["/wdcli","--internal-in-docker","--config", "${CONFIG_PATH}","server","--bind","0.0.0.0:8888"]