Initial status page
This commit is contained in:
parent
a3511b1bfc
commit
a1f35b97d3
17 changed files with 618 additions and 83 deletions
69
cmd/pathbuilders.go
Normal file
69
cmd/pathbuilders.go
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
wisski_distillery "github.com/FAU-CDI/wisski-distillery"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/core"
|
||||||
|
"github.com/tkw1536/goprogram/exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pathbuilders is the 'pathbuilders' command
|
||||||
|
var Pathbuilders wisski_distillery.Command = pathbuilders{}
|
||||||
|
|
||||||
|
type pathbuilders struct {
|
||||||
|
Positionals struct {
|
||||||
|
Slug string `positional-arg-name:"SLUG" required:"1-1" description:"slug of instance to export pathbuilders of"`
|
||||||
|
Name string `positional-arg-name:"NAME" description:"Name of pathbuilder to get. If omitted, show a list of all pathbuilders"`
|
||||||
|
} `positional-args:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pathbuilders) Description() wisski_distillery.Description {
|
||||||
|
return wisski_distillery.Description{
|
||||||
|
Requirements: core.Requirements{
|
||||||
|
NeedsDistillery: true,
|
||||||
|
},
|
||||||
|
Command: "pathbuilder",
|
||||||
|
Description: "Lists of displays pathbuilders of a specific WissKI instance",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errPathbuilders = exit.Error{
|
||||||
|
Message: "Unable to export pathbuilder: %s",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNoPathbuilder = exit.Error{
|
||||||
|
Message: "Pathbuilder %q does not exist",
|
||||||
|
ExitCode: exit.ExitGeneric,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pb pathbuilders) Run(context wisski_distillery.Context) error {
|
||||||
|
// get the wisski
|
||||||
|
instance, err := context.Environment.Instances().WissKI(pb.Positionals.Slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all of the pathbuilders
|
||||||
|
if pb.Positionals.Name == "" {
|
||||||
|
names, err := instance.Pathbuilders()
|
||||||
|
if err != nil {
|
||||||
|
return errPathbuilders.WithMessageF(err)
|
||||||
|
}
|
||||||
|
for _, name := range names {
|
||||||
|
context.Println(name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all the pathbuilders
|
||||||
|
xml, err := instance.Pathbuilder(pb.Positionals.Name)
|
||||||
|
if xml == "" {
|
||||||
|
return errNoPathbuilder.WithMessageF(pb.Positionals.Name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errPathbuilders.WithMessageF(err)
|
||||||
|
}
|
||||||
|
context.Printf("%s", xml)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,7 @@ func init() {
|
||||||
wdcli.Register(cmd.Shell)
|
wdcli.Register(cmd.Shell)
|
||||||
wdcli.Register(cmd.BlindUpdate)
|
wdcli.Register(cmd.BlindUpdate)
|
||||||
wdcli.Register(cmd.UpdatePrefixConfig) // TODO: Move into post-instance configuration
|
wdcli.Register(cmd.UpdatePrefixConfig) // TODO: Move into post-instance configuration
|
||||||
|
wdcli.Register(cmd.Pathbuilders)
|
||||||
|
|
||||||
// backup & cron
|
// backup & cron
|
||||||
wdcli.Register(cmd.Snapshot)
|
wdcli.Register(cmd.Snapshot)
|
||||||
|
|
|
||||||
55
internal/component/dis/html/index.html
Normal file
55
internal/component/dis/html/index.html
Normal 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 }} <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>
|
||||||
16
internal/component/dis/html/instance.html
Normal file
16
internal/component/dis/html/instance.html
Normal 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>
|
||||||
20
internal/component/dis/html/static/autolink.css
Normal file
20
internal/component/dis/html/static/autolink.css
Normal 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;
|
||||||
|
}
|
||||||
24
internal/component/dis/html/static/autolink.js
Normal file
24
internal/component/dis/html/static/autolink.js
Normal 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);
|
||||||
|
}
|
||||||
67
internal/component/dis/html/static/dis.css
Normal file
67
internal/component/dis/html/static/dis.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,70 +1,198 @@
|
||||||
package dis
|
package dis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"embed"
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"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/component/instances"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/config"
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
|
||||||
"github.com/tkw1536/goprogram/stream"
|
"github.com/tkw1536/goprogram/stream"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (dis *Dis) info(io stream.IOStream) (http.Handler, error) {
|
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) {
|
Config *config.Config
|
||||||
// make sure the user is authorized
|
|
||||||
if !dis.authDis(r) {
|
Instances []instances.Info
|
||||||
w.Header().Add("WWW-Authenticate", `Basic realm="WissKI Distillery Admin"`)
|
TotalCount int
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
RunningCount int
|
||||||
w.Write([]byte("Unauthorized"))
|
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
|
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
|
var errgroup errgroup.Group
|
||||||
errgroup.SetLimit(disLimit)
|
|
||||||
|
|
||||||
// list all the instances
|
// list all the instances
|
||||||
all, err := dis.Instances.All()
|
all, err := dis.Instances.All()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
return nil, err
|
||||||
w.Write([]byte("internal server error"))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all of their info!
|
// get all of their info!
|
||||||
infos := make([]instances.Info, len(all))
|
infos = make([]instances.Info, len(all))
|
||||||
for i, instance := range all {
|
for i, instance := range all {
|
||||||
{
|
{
|
||||||
i := i
|
i := i
|
||||||
instance := instance
|
instance := instance
|
||||||
|
|
||||||
errgroup.Go(func() (err error) {
|
errgroup.Go(func() (err error) {
|
||||||
infos[i], err = instance.Info()
|
infos[i], err = instance.Info(true)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if some info call failed
|
// wait for the results, and return
|
||||||
if err := errgroup.Wait(); err != nil {
|
err = errgroup.Wait()
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
w.Write([]byte("internal server error"))
|
|
||||||
w.Write([]byte("\n"))
|
|
||||||
return
|
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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@ FROM docker.io/library/docker:20.10-cli
|
||||||
|
|
||||||
COPY wdcli /wdcli
|
COPY wdcli /wdcli
|
||||||
EXPOSE 8888
|
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"]
|
||||||
|
|
@ -1,17 +1,33 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
|
||||||
* This script will list all the URIs that this system is aware of.
|
|
||||||
* This works by listing all the default graph uris of all the adapters.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use Drupal\wisski_pathbuilder\Entity\WisskiPathEntity;
|
use Drupal\wisski_pathbuilder\Entity\WisskiPathEntity;
|
||||||
|
|
||||||
// load all the pathbuilders
|
/** all_xml lists all pathbuilders, and returns the corresponding xml */
|
||||||
$pbs = \Drupal::entityTypeManager()->getStorage('wisski_pathbuilder')->loadMultiple();
|
function all_xml(): object {
|
||||||
|
$all = \Drupal::entityTypeManager()->getStorage('wisski_pathbuilder')->loadMultiple();
|
||||||
|
return (object)array_map("entity_to_xml", $all);
|
||||||
|
}
|
||||||
|
|
||||||
// map over the pathbuilders
|
|
||||||
$xmls = array_map(function($pb) {
|
/** all_list lists the ids of all pathbuilders */
|
||||||
|
function all_list(): Array {
|
||||||
|
return array_keys(\Drupal::entityQuery('wisski_pathbuilder')->execute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** one_xml serializes a single pathbuilder as xml */
|
||||||
|
function one_xml(string $id): string {
|
||||||
|
$pb = \Drupal::entityTypeManager()->getStorage('wisski_pathbuilder')->load($id);
|
||||||
|
if ($pb === NULL) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return entity_to_xml($pb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================================
|
||||||
|
// =================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
function entity_to_xml($pb) {
|
||||||
$xml = new \SimpleXMLElement("<pathbuilderinterface></pathbuilderinterface>");
|
$xml = new \SimpleXMLElement("<pathbuilderinterface></pathbuilderinterface>");
|
||||||
|
|
||||||
$paths = $pb->getAllPaths();
|
$paths = $pb->getAllPaths();
|
||||||
|
|
@ -58,6 +74,4 @@ $xmls = array_map(function($pb) {
|
||||||
$dom = dom_import_simplexml($xml)->ownerDocument;
|
$dom = dom_import_simplexml($xml)->ownerDocument;
|
||||||
$dom->formatOutput = TRUE;
|
$dom->formatOutput = TRUE;
|
||||||
return $dom->saveXML();
|
return $dom->saveXML();
|
||||||
}, $pbs);
|
}
|
||||||
|
|
||||||
echo json_encode($xmls);
|
|
||||||
|
|
@ -1,34 +1,45 @@
|
||||||
package instances
|
package instances
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
"github.com/tkw1536/goprogram/stream"
|
"github.com/tkw1536/goprogram/stream"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errPathbuildersExecFailed = errors.New("ExportPathbuilders: Failed to call export_pathbuilder")
|
//go:embed php/export_pathbuilder.php
|
||||||
|
var exportPathbuilderPHP string
|
||||||
|
|
||||||
|
// Pathbuilders returns the ids of all pathbuilders in consistent order.
|
||||||
|
func (wisski *WissKI) Pathbuilders() (ids []string, err error) {
|
||||||
|
err = wisski.ExecPHPScript(stream.FromNil(), &ids, exportPathbuilderPHP, "all_list")
|
||||||
|
slices.Sort(ids)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pathbuilder returns a single pathbuilder as xml.
|
||||||
|
// If it does not exist, it returns the empty string and nil error.
|
||||||
|
func (wisski *WissKI) Pathbuilder(id string) (xml string, err error) {
|
||||||
|
err = wisski.ExecPHPScript(stream.FromNil(), &xml, exportPathbuilderPHP, "one_xml", id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllPathbuilders returns all pathbuilders serialized as xml
|
||||||
|
func (wisski *WissKI) AllPathbuilders() (pathbuilders map[string]string, err error) {
|
||||||
|
err = wisski.ExecPHPScript(stream.FromNil(), &pathbuilders, exportPathbuilderPHP, "all_xml")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ExportPathbuilders writes pathbuilders into the directory dest
|
// ExportPathbuilders writes pathbuilders into the directory dest
|
||||||
func (wisski *WissKI) ExportPathbuilders(dest string) error {
|
func (wisski *WissKI) ExportPathbuilders(dest string) error {
|
||||||
// export all the pathbuilders into the buffer
|
pathbuilders, err := wisski.AllPathbuilders()
|
||||||
var buffer bytes.Buffer
|
if err != nil {
|
||||||
wu := stream.NewIOStream(&buffer, nil, nil, 0)
|
|
||||||
code, err := wisski.Barrel().Exec(wu, "barrel", "/bin/bash", "/user_shell.sh", "-c", "drush php:script /wisskiutils/export_pathbuilder.php")
|
|
||||||
if err != nil || code != 0 {
|
|
||||||
return errPathbuildersExecFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode them as a json array
|
|
||||||
var pathbuilders map[string]string
|
|
||||||
if err := json.NewDecoder(&buffer).Decode(&pathbuilders); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,12 @@ var ErrExecInvalidCode = errors.New("invalid code to execute")
|
||||||
var ErrExecNonZero = errors.New("script returned non-zero code")
|
var ErrExecNonZero = errors.New("script returned non-zero code")
|
||||||
|
|
||||||
// ExecPHPScript executes the PHP code as a script within the wisski instance.
|
// ExecPHPScript executes the PHP code as a script within the wisski instance.
|
||||||
// The script should define a function "main", and may define additional functions.
|
// The script should define a function called entrypoint, and may define additional functions.
|
||||||
//
|
//
|
||||||
// Code must start with "<?php" and may not contain a closing tag.
|
// Code must start with "<?php" and may not contain a closing tag.
|
||||||
// Code is expected not to mess with PHPs output buffer.
|
// Code is expected not to mess with PHPs output buffer.
|
||||||
// Code should not contain user input.
|
// Code should not contain user input.
|
||||||
|
// Code breaking these conventions may or may not result in an error.
|
||||||
//
|
//
|
||||||
// It's arguments are encoded as json using [json.Marshal] and decoded within php.
|
// It's arguments are encoded as json using [json.Marshal] and decoded within php.
|
||||||
//
|
//
|
||||||
|
|
@ -25,10 +26,10 @@ var ErrExecNonZero = errors.New("script returned non-zero code")
|
||||||
//
|
//
|
||||||
// Standard input and output streams should not be used.
|
// Standard input and output streams should not be used.
|
||||||
// Standard error is redirected to io.
|
// Standard error is redirected to io.
|
||||||
func (wisski *WissKI) ExecPHPScript(io stream.IOStream, code string, args ...any) (any, error) {
|
func (wisski *WissKI) ExecPHPScript(io stream.IOStream, result any, code string, entrypoint string, args ...any) error {
|
||||||
// make sure the beginning is right
|
// make sure the beginning is right
|
||||||
if !strings.HasPrefix(code, "<?php") {
|
if !strings.HasPrefix(code, "<?php") {
|
||||||
return nil, ErrExecInvalidCode
|
return ErrExecInvalidCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure that args is not nil, but an array of length 0!
|
// make sure that args is not nil, but an array of length 0!
|
||||||
|
|
@ -36,15 +37,20 @@ func (wisski *WissKI) ExecPHPScript(io stream.IOStream, code string, args ...any
|
||||||
args = []any{}
|
args = []any{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode code and args!
|
// encode code, args and entrypoint!
|
||||||
codeEscape, err := marshalPHP("?>" + code)
|
codeEscape, err := marshalPHP("?>" + code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entrypointEscape, err := marshalPHP(entrypoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
argsEscape, err := marshalPHP(args)
|
argsEscape, err := marshalPHP(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// assemble the script
|
// assemble the script
|
||||||
|
|
@ -55,7 +61,7 @@ func (wisski *WissKI) ExecPHPScript(io stream.IOStream, code string, args ...any
|
||||||
|
|
||||||
call_user_func(function(){
|
call_user_func(function(){
|
||||||
ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE);
|
ob_start(null, 0, PHP_OUTPUT_HANDLER_CLEANABLE);
|
||||||
$result = call_user_func_array("main", ` + argsEscape + `);
|
$result = call_user_func_array(` + entrypointEscape + `, ` + argsEscape + `);
|
||||||
ob_end_clean();
|
ob_end_clean();
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
});
|
});
|
||||||
|
|
@ -65,22 +71,19 @@ func (wisski *WissKI) ExecPHPScript(io stream.IOStream, code string, args ...any
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
res, err := wisski.Shell(io.Streams(&output, nil, strings.NewReader(script), 0), "-c", "drush php:script -")
|
res, err := wisski.Shell(io.Streams(&output, nil, strings.NewReader(script), 0), "-c", "drush php:script -")
|
||||||
if res != 0 {
|
if res != 0 {
|
||||||
return nil, ErrExecNonZero
|
return ErrExecNonZero
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// did not request to receive a result
|
||||||
|
if result == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode the output
|
// decode the output
|
||||||
var result any
|
return json.NewDecoder(&output).Decode(result)
|
||||||
err = json.NewDecoder(&output).Decode(&result)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// EvalPHP is similar to ExecPHPScript, except that it evaluates a single line of php.
|
|
||||||
// A single parameter may be passed, which can be accessed using the name $arg inside the expression.
|
|
||||||
func (wisski *WissKI) EvalPHP(expr string, arg any) (any, error) {
|
|
||||||
return wisski.ExecPHPScript(stream.FromEnv(), "function main($arg){return "+expr+";}", arg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const marshalRune = 'F' // press to pay respect
|
const marshalRune = 'F' // press to pay respect
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,36 @@ import (
|
||||||
// Info represents some info about this WissKI
|
// Info represents some info about this WissKI
|
||||||
type Info struct {
|
type Info struct {
|
||||||
Slug string // The slug of the instance
|
Slug string // The slug of the instance
|
||||||
|
URL string // The public URL of this instance
|
||||||
|
|
||||||
Running bool // is the instance running?
|
Running bool // is the instance running?
|
||||||
|
Pathbuilders []string // list of pathbuilders
|
||||||
DrupalVersion interface{} // version of drupal being used
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info returns information about this WissKI instance.
|
// Info returns information about this WissKI instance.
|
||||||
func (wisski *WissKI) Info() (info Info, err error) {
|
func (wisski *WissKI) Info(quick bool) (info Info, err error) {
|
||||||
// static properties
|
// static properties
|
||||||
info.Slug = wisski.Slug
|
info.Slug = wisski.Slug
|
||||||
|
info.URL = wisski.URL().String()
|
||||||
|
|
||||||
// dynamic properties, TODO: Add more properties here!
|
// dynamic properties, TODO: Add more properties here!
|
||||||
var group errgroup.Group
|
var group errgroup.Group
|
||||||
|
|
||||||
|
// quick check if this wisski is running
|
||||||
group.Go(func() (err error) {
|
group.Go(func() (err error) {
|
||||||
info.Running, err = wisski.Alive()
|
info.Running, err = wisski.Alive()
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// slower checks for extra properties.
|
||||||
|
// these execute php code
|
||||||
|
if !quick {
|
||||||
|
group.Go(func() (err error) {
|
||||||
|
info.Pathbuilders, err = wisski.Pathbuilders()
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
err = group.Wait()
|
err = group.Wait()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
pkg/httpx/basic.go
Normal file
24
pkg/httpx/basic.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package httpx
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
var basicUnauthorized = []byte("Unauthorized")
|
||||||
|
|
||||||
|
// BasicAuth returns a new [http.Handler] that requires any credentials to pass the check function
|
||||||
|
func BasicAuth(handler http.Handler, realm string, check func(username, password string) bool) http.Handler {
|
||||||
|
var authenticateHeader = `Basic realm="` + realm + `"`
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// if the basic authentication passes
|
||||||
|
// we can just use the handler!
|
||||||
|
user, pass, ok := r.BasicAuth()
|
||||||
|
if ok && check(user, pass) {
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// http authentication did not pass
|
||||||
|
w.Header().Add("WWW-Authenticate", authenticateHeader)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write(basicUnauthorized)
|
||||||
|
})
|
||||||
|
}
|
||||||
41
pkg/httpx/html.go
Normal file
41
pkg/httpx/html.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTMLHandler[T any] struct {
|
||||||
|
Handler func(r *http.Request) (T, error)
|
||||||
|
Template *template.Template // called with T
|
||||||
|
}
|
||||||
|
|
||||||
|
var htmlInternalServerErr = []byte(`<!DOCTYPE HTML><title>Internal Server Error</title>Internal Server Error`)
|
||||||
|
var htmlNotFound = []byte(`<!DOCTYPE HTML><title>Not Found</title>Not Found`)
|
||||||
|
|
||||||
|
// ServeHTTP calls j(r) and returns json
|
||||||
|
func (h HTMLHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// call the function
|
||||||
|
result, err := h.Handler(r)
|
||||||
|
|
||||||
|
// entity not found
|
||||||
|
if err == ErrNotFound {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write(htmlNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle other errors
|
||||||
|
if err != nil {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(htmlInternalServerErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out the response as json
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
h.Template.Execute(w, result)
|
||||||
|
}
|
||||||
6
pkg/httpx/httpx.go
Normal file
6
pkg/httpx/httpx.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package httpx
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// ErrNotFound should be returned from any httpx error to indicate that the item was not found
|
||||||
|
var ErrNotFound = errors.New("httpx: Error 404")
|
||||||
45
pkg/httpx/json.go
Normal file
45
pkg/httpx/json.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsonInternalServerErr = []byte(`{"status":"internal server error"}`)
|
||||||
|
var jsonNotFound = []byte(`{"status":"not found"}`)
|
||||||
|
|
||||||
|
// JSON creates a new JSONHandler
|
||||||
|
func JSON[T any](f func(r *http.Request) (T, error)) JSONHandler[T] {
|
||||||
|
return JSONHandler[T](f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONHandler implements [http.Handler] by returning values as json to the caller.
|
||||||
|
// In case of an error, a generic "internal server error" message is returned.
|
||||||
|
type JSONHandler[T any] func(r *http.Request) (T, error)
|
||||||
|
|
||||||
|
// ServeHTTP calls j(r) and returns json
|
||||||
|
func (j JSONHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// call the function
|
||||||
|
result, err := j(r)
|
||||||
|
|
||||||
|
// entity not found
|
||||||
|
if err == ErrNotFound {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write(jsonNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle other errors
|
||||||
|
if err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write(jsonInternalServerErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out the response as json
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue