Rename info -> admin

This commit is contained in:
Tom Wiesing 2023-01-04 12:53:02 +01:00
parent 785130dc36
commit 11f7749c1d
No known key found for this signature in database
10 changed files with 55 additions and 55 deletions

View file

@ -0,0 +1,102 @@
package admin
import (
"context"
"net/http"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
"github.com/julienschmidt/httprouter"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
)
type Admin struct {
component.Base
Dependencies struct {
Fetchers []component.DistilleryFetcher
Exporter *exporter.Exporter
Instances *instances.Instances
SnapshotsLog *logger.Logger
Auth *auth.Auth
}
Analytics *lazy.PoolAnalytics
}
var (
_ component.DistilleryFetcher = (*Admin)(nil)
_ component.Routeable = (*Admin)(nil)
)
func (*Admin) Routes() []string { return []string{"/admin/"} }
func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) {
router := httprouter.New()
{
socket := &httpx.WebSocket{
Context: ctx,
Fallback: router,
Handler: admin.serveSocket,
}
handler = admin.Dependencies.Auth.Protect(socket, auth.Admin)
}
// handle everything
router.HandlerFunc(http.MethodGet, route, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, route+"index", http.StatusTemporaryRedirect)
})
// add a handler for the index page
router.Handler(http.MethodGet, route+"index", httpx.HTMLHandler[indexContext]{
Handler: admin.index,
Template: indexTemplate,
})
// add a handler for the component page
router.Handler(http.MethodGet, route+"components", httpx.HTMLHandler[componentContext]{
Handler: admin.components,
Template: componentsTemplate,
})
// add a handler for the component page
router.Handler(http.MethodGet, route+"ingredients/:slug", httpx.HTMLHandler[ingredientsContext]{
Handler: admin.ingredients,
Template: ingredientsTemplate,
})
// add a handler for the instance page
router.Handler(http.MethodGet, route+"instance/:slug", httpx.HTMLHandler[instanceContext]{
Handler: admin.instance,
Template: instanceTemplate,
})
router.Handler(http.MethodPost, route+"api/login", httpx.RedirectHandler(func(r *http.Request) (string, int, error) {
// parse the form
if err := r.ParseForm(); err != nil {
return "", 0, err
}
// get the instance
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), r.PostFormValue("slug"))
if err != nil {
return "", 0, httpx.ErrNotFound
}
target, err := instance.Users().Login(r.Context(), nil, r.PostFormValue("user"))
if err != nil {
return "", 0, err
}
return target.String(), http.StatusSeeOther, err
}))
return
}

View file

@ -0,0 +1,68 @@
package admin
import (
"net/http"
"time"
_ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/gorilla/mux"
)
//go:embed "html/components.html"
var componentsTemplateString string
var componentsTemplate = static.AssetsComponentsIndex.MustParseShared(
"components.html",
componentsTemplateString,
)
type componentContext struct {
Time time.Time
Analytics lazy.PoolAnalytics
}
func (admin *Admin) components(r *http.Request) (cp componentContext, err error) {
cp.Analytics = *admin.Analytics
cp.Time = time.Now().UTC()
return
}
//go:embed "html/ingredients.html"
var ingredientsTemplateString string
var ingredientsTemplate = static.AssetsInstanceComponentsIndex.MustParseShared(
"ingredients.html",
ingredientsTemplateString,
)
type ingredientsContext struct {
Time time.Time
Instance models.Instance
Analytics *lazy.PoolAnalytics
}
func (admin *Admin) ingredients(r *http.Request) (cp ingredientsContext, err error) {
cp.Time = time.Now().UTC()
// find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), mux.Vars(r)["slug"])
if err == instances.ErrWissKINotFound {
return cp, httpx.ErrNotFound
}
if err != nil {
return cp, err
}
cp.Instance = instance.Instance
// and get the components
cp.Analytics = instance.Info().Analytics
return
}

View file

@ -0,0 +1,13 @@
{{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - Components Page{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/components">Components</a>
</p>
{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -0,0 +1,277 @@
{{ template "_base.html" . }}
{{ define "title" }}Distillery Admin{{ end }}
{{ define "header"}}
<p>
<a class="pure-button pure-button-primary" href="/admin/index">Admin</a>
</p>
<p>
<a class="pure-button" href="/admin/components">Components</a>
</p>
{{ end }}
{{ define "content" }}
<div class="pure-u-1-1">
<h2 id="overview">Distillery Configuration</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Domains
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Primary
</td>
<td>
<code>{{.Config.DefaultDomain}}</code>
</td>
</tr>
<tr>
<td>
Extra
</td>
<td>
{{ range .Config.SelfExtraDomains }}
<code>{{.}}</code><br />
{{ end }}
</td>
</tr>
<tr>
<td>
Email <small>(HTTPS)</small>
</td>
<td>
<code>{{.Config.CertbotEmail}}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Database Settings
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
MySQL User Prefix
</td>
<td>
<code>{{.Config.MysqlUserPrefix}}</code>
</td>
</tr>
<tr>
<td>
MySQL Database Prefix
</td>
<td>
<code>{{.Config.MysqlDatabasePrefix}}</code>
</td>
</tr>
<tr>
<td>
GraphDB User Prefix
</td>
<td>
<code>{{.Config.GraphDBUserPrefix}}</code>
</td>
</tr>
<tr>
<td>
GraphDB Database Prefix
</td>
<td>
<code>{{.Config.GraphDBRepoPrefix}}</code>
</td>
</tr>
<tr>
<td>
Bookkeeping Database
</td>
<td>
<code>{{.Config.DistilleryDatabase}}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-3">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Directory Settings
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>root</code>
</td>
<td>
<code>{{.Config.DeployRoot}}</code>
</td>
</tr>
<tr>
<td>
<code>config</code>
</td>
<td>
<code>{{.Config.ConfigPath}}</code>
</td>
</tr>
<tr>
<td>
<code>authorized_keys</code>
</td>
<td>
<code>{{.Config.GlobalAuthorizedKeysFile}}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Misc Settings
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Homepage
</td>
<td>
<a href="{{.Config.SelfRedirect}}" target="_blank" rel="noopener noreferrer">{{.Config.SelfRedirect}}</a>
</td>
</tr>
<tr>
<td>
Docker Network Name
</td>
<td>
<code>{{.Config.DockerNetworkName}}</code>
</td>
</tr>
<tr>
<td>
Backup Age
</td>
<td>
<code>{{.Config.MaxBackupAge}}</code> Day(s)
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="backups">Backups</h2>
</div>
<div class="pure-u-1">
<table class="pure-table pure-table-bordered padding">
<thead>
<tr>
<th>Path</th>
<th>Created</th>
<th>Packed</th>
</tr>
</thead>
<tbody>
{{ range .Backups }}
<tr>
<td>
<code class="path">{{ .Path }}</code>
</td>
<td>
<code class="date">{{ .Created.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
{{ .Packed }}
</td>
</tr>
{{ end}}
</tbody>
</table>
</div>
<div class="pure-u-1">
<h2 id="instances">Instances</h2>
<table class="pure-table pure-table-bordered padding">
<thead>
<tr>
<th>Total</th>
<th>Running</th>
<th>Stopped</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{{ .TotalCount }}
</td>
<td>
{{ .RunningCount }}
</td>
<td>
{{ .StoppedCount }}
</td>
</tr>
</tbody>
</table>
<span class="hspace"></span>
</div>
{{range .Instances}}
<div class="pure-u-1 pure-u-xl-1-3">
<div class="wisski {{ if .Running }}running{{ else }}stopped{{ end }}">
<h3>
{{.Slug}}
{{ if not .Running }}&nbsp;<small>not running</small>{{ end }}
</h3>
<p>
<a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.URL}}</a><br>
<a class="pure-button" href="/admin/instance/{{.Slug}}">Details</a>
</p>
</div>
</div>
{{end}}
{{ end }}

View file

@ -0,0 +1,14 @@
{{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - {{ .Instance.Slug }} - Ingredients{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Admin</a> &gt;
<a class="pure-button" href="/admin/instance/{{ .Instance.Slug }}">Instance</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/ingredients/{{ .Instance.Slug }}">Ingredients</a>
</p>
{{ end }}
{{ define "content" }}
{{ template "_anal.html" .Analytics }}
{{ end }}

View file

@ -0,0 +1,509 @@
{{ template "_base.html" . }}
{{ define "title" }}Distillery Admin - {{ .Info.Slug }}{{ end }}
{{ define "header"}}
<p>
<a class="pure-button" href="/admin/index">Control</a> &gt;
<a class="pure-button pure-button-primary" href="/admin/instance/{{ .Info.Slug }}">Instance</a>
</p>
<p>
<a class="pure-button" href="/admin/ingredients/{{ .Info.Slug }}">Ingredients</a>
</p>
{{ end }}
{{ define "content" }}
<div class="pure-u-1-1">
<h2 id="overview">Info &amp; Status</h2>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Overview
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Slug
</td>
<td>
<code>{{ .Info.Slug }}</code>
</td>
</tr>
<tr>
<td>
URL
</td>
<td>
<a href="{{ .Info.URL }}" target="_blank" rel="noopener noreferrer">{{ .Info.URL }}</a>
</td>
</tr>
<tr>
<td>
Running
</td>
<td>
<code>{{ .Info.Running }}</code>
</td>
</tr>
<tr>
<td>
Locked
</td>
<td>
<code>{{ .Info.Locked }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Component Settings
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Directory
</td>
<td>
<code style="overflow: auto;">{{ .Instance.FilesystemBase }}</code>
</td>
</tr>
<tr>
<td>
SQL DB
</td>
<td>
<code>{{ .Instance.SqlDatabase }}</code>
</td>
</tr>
<tr>
<td>
SQL User
</td>
<td>
<code>{{ .Instance.SqlUsername }}</code>
</td>
</tr>
<tr>
<td>
TS Repo
</td>
<td>
<code>{{ .Instance.GraphDBRepository }}</code>
</td>
</tr>
<tr>
<td>
TS User
</td>
<td>
<code>{{ .Instance.GraphDBUsername }}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-2-5">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Build Status
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Created
</td>
<td>
<code class="date">{{ .Instance.Created.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
</tr>
<tr>
<td>
Last Rebuild <br>
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Rebuild</button>
</td>
<td>
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
</tr>
<tr>
<td>
Last Cron<br>
<button class="remote-action pure-button pure-button-action" data-action="cron" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Cron</button>
</td>
<td>
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
</tr>
<tr>
<td>
Last Update <br>
<button class="remote-action pure-button pure-button-action" data-action="update" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Update</button>
</td>
<td>
<code class="date">{{ .Info.LastUpdate.Format "2006-01-02T15:04:05Z07:00" }}</code><br>
(Automatic: <code>{{ .Instance.AutoBlindUpdateEnabled }}</code>)
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="wisski">(Drupal) Users</h2>
</div>
<div class="pure-u-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
ID
</th>
<th>
Active
</th>
<th>
Name
</th>
<th>
Email
</th>
<th>
Roles
</th>
<th>
Created
</th>
<th>
Last Login
</th>
<th>
Action
</th>
</tr>
</thead>
<tbody>
{{ $slug := .Instance.Slug }}
{{ range $index, $user := .Info.Users }}
<tr {{ if not $user.Status }}style="color:gray"{{ end }}>
<td>
<code>{{ $user.UID }}</code>
</td>
<td>
<code>{{ $user.Status }}</code>
</td>
<td>
<code>{{ $user.Name }}</code>
</td>
<td>
{{ if $user.Mail }}
<a href="mailto:{{ $user.Mail }}">{{ $user.Mail }}</a>
{{ end }}
</td>
<td>
{{ range $role, $unuused := $user.Roles }}
<code>
{{ $role }}
</code>
{{ end }}
</td>
<td>
<code class="date">{{ $user.Created.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
<code class="date">{{ $user.Login.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
<form action="/admin/api/login" method="POST" target="_blank">
<input type="hidden" name="slug" value="{{ $slug }}">
<input type="hidden" name="user" value="{{ $user.Name }}">
<input type="submit" class="pure-button pure-button-action" value="Login in new window">
</form>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="distillery">(Distillery) Users</h2>
</div>
<div class="pure-u-1">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
Distillery User
</th>
<th>
WissKI User
</th>
<th>
Admin
</th>
</tr>
</thead>
<tbody>
{{ range $index, $grant := .Info.Grants }}
<tr>
<td>
<code>{{ $grant.User }}</code>
</td>
<td>
<code>{{ $grant.DrupalUsername }}</code>
</td>
<td>
<code>{{ $grant.DrupalAdminRole }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="wisski">WissKI Data</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Pathbuilders
</th>
</tr>
</thead>
<tbody>
{{ range $name, $xml := .Info.Pathbuilders }}
<tr>
<td>
<code>{{ $name }}</code>
</td>
<td>
<code class="pathbuilder" data-name="{{ $name }}">{{ $xml }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th>
URI Prefixes
{{ if .Info.NoPrefixes }}
(excluded from resolver)
{{ end }}
</th>
</tr>
</thead>
<tbody>
{{ range $index, $prefix := .Info.Prefixes }}
<tr>
<td>
<code>{{ $prefix }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="stats">Statistics</h2>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="5">
Bundles
</th>
</tr>
<tr>
<th>
Label
</th>
<th>
Machine Name
</th>
<th>
Count
</th>
<th>
LastEdit
</th>
<th>
MainBundle
</th>
</tr>
</thead>
<tbody>
{{ range .Info.Statistics.Bundles.Bundles }}
<tr>
<td>
<code>{{ .Label }}</code>
</td>
<td>
<code>{{ .MachineName }}</code>
</td>
<td>
<code>{{ .Count }}</code>
</td>
<td>
<code class="date">{{ .LastEdit.Time.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
<code>{{ .MainBundle }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1 pure-u-xl-1-2">
<div class="padding">
<div class="overflow">
<table class="pure-table pure-table-bordered">
<thead>
<tr>
<th colspan="2">
Triplestore
</th>
</tr>
<tr>
<th>
URI
</th>
<th>
Count
</th>
</tr>
</thead>
<tbody>
{{ range .Info.Statistics.Triplestore.Graphs }}
<tr>
<td>
<code>{{ .URI }}</code>
</td>
<td>
<code>{{ .Count }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
<div class="pure-u-1-1">
<h2 id="ssh">SSH Keys</h2>
<table class="pure-table pure-table-bordered padding">
<tbody>
{{ range .Info.SSHKeys }}
<tr>
<td>
<code>{{ . }}</code>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="pure-u-1-1">
<h2 id="snapshots">Snapshots</h2>
<p>
<button class="remote-action pure-button pure-button-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">Take a snapshot</button>
</p>
</div>
<div class="pure-u-1">
<table class="pure-table pure-table-bordered padding">
<thead>
<tr>
<th>Path</th>
<th>Created</th>
<th>Packed</th>
</tr>
</thead>
<tbody>
{{ range .Info.Snapshots }}
<tr>
<td>
<code class="path">{{ .Path }}</code>
</td>
<td>
<code class="date">{{ .Created.Format "2006-01-02T15:04:05Z07:00" }}</code>
</td>
<td>
{{ .Packed }}
</td>
</tr>
{{ end}}
</tbody>
</table>
</div>
{{ end }}

View file

@ -0,0 +1,95 @@
package admin
import (
"context"
"net/http"
"time"
_ "embed"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"golang.org/x/sync/errgroup"
)
//go:embed "html/index.html"
var indexTemplateStr string
var indexTemplate = static.AssetsControlIndex.MustParseShared(
"index.html",
indexTemplateStr,
)
// Status produces a new observation of the distillery, and a new information of all instances
// The information on all instances is passed the given quick flag.
func (admin *Admin) Status(ctx context.Context, QuickInformation bool) (target status.Distillery, information []status.WissKI, err error) {
var group errgroup.Group
group.Go(func() error {
// list all the instances
all, err := admin.Dependencies.Instances.All(ctx)
if err != nil {
return err
}
// get all of their info!
information = make([]status.WissKI, len(all))
for i, instance := range all {
{
i := i
instance := instance
// store the info for this group!
group.Go(func() (err error) {
information[i], err = instance.Info().Information(ctx, true)
return err
})
}
}
return nil
})
// gather all the observations
flags := component.FetcherFlags{
Context: ctx,
}
for _, o := range admin.Dependencies.Fetchers {
o := o
group.Go(func() error {
return o.Fetch(flags, &target)
})
}
// wait for all the fetchers to finish
if err := group.Wait(); err != nil {
return status.Distillery{}, nil, err
}
// count overall instances
for _, i := range information {
if i.Running {
target.RunningCount++
} else {
target.StoppedCount++
}
}
target.TotalCount = len(information)
return
}
type indexContext struct {
status.Distillery
Instances []status.WissKI
}
func (admin *Admin) index(r *http.Request) (idx indexContext, err error) {
idx.Distillery, idx.Instances, err = admin.Status(r.Context(), true)
return
}
func (admin *Admin) Fetch(flags component.FetcherFlags, target *status.Distillery) error {
target.Time = time.Now().UTC()
target.Config = admin.Config
return nil
}

View file

@ -0,0 +1,51 @@
package admin
import (
_ "embed"
"net/http"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/FAU-CDI/wisski-distillery/internal/status"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/gorilla/mux"
)
//go:embed "html/instance.html"
var instanceTemplateString string
var instanceTemplate = static.AssetsControlInstance.MustParseShared(
"instance.html",
instanceTemplateString,
)
type instanceContext struct {
Time time.Time
Instance models.Instance
Info status.WissKI
}
func (admin *Admin) instance(r *http.Request) (is instanceContext, err error) {
// find the instance itself!
instance, err := admin.Dependencies.Instances.WissKI(r.Context(), mux.Vars(r)["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().Information(r.Context(), false)
if err != nil {
return is, err
}
// current time
is.Time = time.Now().UTC()
return
}

View file

@ -0,0 +1,138 @@
package admin
import (
"context"
"encoding/json"
"fmt"
"io"
"time"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
"github.com/FAU-CDI/wisski-distillery/pkg/httpx"
"github.com/tkw1536/goprogram/status"
)
type InstanceAction struct {
NumParams int
HandleInteractive func(ctx context.Context, info *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error
HandleResult func(ctx context.Context, info *Admin, instance *wisski.WissKI, params ...string) (value any, err error)
}
var socketInstanceActions = map[string]InstanceAction{
"snapshot": {
HandleInteractive: func(ctx context.Context, info *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
return info.Dependencies.Exporter.MakeExport(
ctx,
out,
exporter.ExportTask{
Dest: "",
Instance: instance,
StagingOnly: false,
},
)
},
},
"rebuild": {
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
return instance.Barrel().Build(ctx, out, true)
},
},
"update": {
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
return instance.Drush().Update(ctx, out)
},
},
"cron": {
HandleInteractive: func(ctx context.Context, _ *Admin, instance *wisski.WissKI, str io.Writer, params ...string) error {
return instance.Drush().Cron(ctx, str)
},
},
}
func (admin *Admin) serveSocket(conn httpx.WebSocketConnection) {
// read the next message to act on
message, ok := <-conn.Read()
if !ok {
return
}
// perform an action if it exists!
if action, ok := socketInstanceActions[string(message.Bytes)]; ok {
admin.handleInstanceAction(conn, action)
return
}
}
var instanceParamsTimeout = time.Second
func (admin *Admin) handleInstanceAction(conn httpx.WebSocketConnection, action InstanceAction) {
// read the slug
slug, ok := <-conn.Read()
if !ok {
<-conn.WriteText("Error reading slug")
return
}
// resolve the instance
instance, err := admin.Dependencies.Instances.WissKI(conn.Context(), string(slug.Bytes))
if err != nil {
<-conn.WriteText("Instance not found")
return
}
// read the parameters
params := make([]string, action.NumParams)
for i := range params {
select {
case message, ok := <-conn.Read():
if !ok {
<-conn.WriteText("Insufficient parameters")
return
}
params[i] = string(message.Bytes)
case <-time.After(instanceParamsTimeout):
<-conn.WriteText("Timed out reading parameters")
return
}
}
// build a stream
writer := &status.LineBuffer{
Line: func(line string) {
<-conn.WriteText(line)
},
FlushLineOnClose: true,
}
defer writer.Close()
// handle the interactive action
if action.HandleInteractive != nil {
err := action.HandleInteractive(conn.Context(), admin, instance, writer, params...)
if err != nil {
fmt.Fprintln(writer, err)
return
}
fmt.Fprintln(writer, "done")
}
// handle the result computation
if action.HandleResult != nil {
result, err := action.HandleResult(conn.Context(), admin, instance, params...)
if err != nil {
fmt.Fprintln(writer, "false")
return
}
data, err := json.Marshal(result)
if err != nil {
fmt.Fprintln(writer, "false")
return
}
fmt.Fprintln(writer, "true")
fmt.Fprintln(writer, data)
}
}