admin: Add purge interface
This commit adds a new option to the admin interface to purge an instance. This requires the user to manually confirm the name of the instance.
This commit is contained in:
parent
3321b5d0ba
commit
2384ee0841
16 changed files with 175 additions and 84 deletions
|
|
@ -1,7 +1,7 @@
|
|||
package cli
|
||||
|
||||
// ===========================================================================================================
|
||||
// This file was generated automatically at 15-01-2023 11:33:49 using gogenlicense.
|
||||
// This file was generated automatically at 16-01-2023 17:01:43 using gogenlicense.
|
||||
// Do not edit manually, as changes may be overwritten.
|
||||
// ===========================================================================================================
|
||||
|
||||
|
|
@ -2417,7 +2417,7 @@ package cli
|
|||
// # Generation
|
||||
//
|
||||
// This variable and the associated documentation have been automatically generated using the 'gogenlicense' tool.
|
||||
// It was last updated at 15-01-2023 11:33:49.
|
||||
// It was last updated at 16-01-2023 17:01:43.
|
||||
var LegalNotices string
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/control/static/custom"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
|
|
@ -32,6 +33,8 @@ type Admin struct {
|
|||
Policy *policy.Policy
|
||||
|
||||
Custom *custom.Custom
|
||||
|
||||
Purger *purger.Purger
|
||||
}
|
||||
|
||||
Analytics *lazy.PoolAnalytics
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@
|
|||
<td>
|
||||
<code>{{ .Info.Running }}</code>
|
||||
<div class="pure-button-group" role="group">
|
||||
<button class="remote-action pure-button pure-button-action" data-action="start" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">
|
||||
<button class="remote-action pure-button pure-button-action" data-action="start" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>
|
||||
(Re)Start
|
||||
</button>
|
||||
<button class="remote-action pure-button pure-button-danger" data-action="stop" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload="true">
|
||||
<button class="remote-action pure-button pure-button-danger" data-action="stop" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
<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>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="rebuild" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Rebuild</button>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
<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>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="cron" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Cron</button>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }}</code>
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
<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>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="update" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Update</button>
|
||||
</td>
|
||||
<td>
|
||||
<code class="date">{{ .Info.LastUpdate.Format "2006-01-02T15:04:05Z07:00" }}</code><br>
|
||||
|
|
@ -437,7 +437,7 @@
|
|||
<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>
|
||||
<button class="remote-action pure-button pure-button-action" data-action="snapshot" data-param="{{ .Instance.Slug }}" data-buffer="1000" data-force-reload>Take a snapshot</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -467,4 +467,22 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1-1">
|
||||
<h2 id="overview">Dangerous Actions</h2>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-xl-2-5">
|
||||
<p>
|
||||
Purging this instance completely removes it from the distillery.
|
||||
Backups containing the instance will remain, but it will not be possible to restore it directly.
|
||||
You must enter the slug <code>{{ .Instance.Slug }}</code> to confirm purging.
|
||||
</p>
|
||||
<form class="pure-form">
|
||||
<fieldset>
|
||||
<input type="text" id="purge-confirm-slug" placeholder="{{ .Instance.Slug }}" />
|
||||
<button class="remote-action pure-button pure-button-danger" data-action="purge" data-param="{{ .Instance.Slug }}" data-confirm-param="#purge-confirm-slug" data-buffer="1000" data-force-reload="/admin/">Purge Instance</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
@ -22,8 +22,8 @@ type InstanceAction struct {
|
|||
|
||||
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(
|
||||
HandleInteractive: func(ctx context.Context, admin *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return admin.Dependencies.Exporter.MakeExport(
|
||||
ctx,
|
||||
out,
|
||||
exporter.ExportTask{
|
||||
|
|
@ -60,6 +60,11 @@ var socketInstanceActions = map[string]InstanceAction{
|
|||
return instance.Barrel().Stack().Down(ctx, out)
|
||||
},
|
||||
},
|
||||
"purge": {
|
||||
HandleInteractive: func(ctx context.Context, admin *Admin, instance *wisski.WissKI, out io.Writer, params ...string) error {
|
||||
return admin.Dependencies.Purger.Purge(ctx, out, instance.Slug)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (admin *Admin) serveSocket(conn httpx.WebSocketConnection) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Removing instances from admin interface
|
||||
date: 2023-01-16
|
||||
---
|
||||
|
||||
- added an option to purge and remove instances from the admin page
|
||||
|
|
@ -21,6 +21,6 @@ var AssetsUser = Assets{
|
|||
|
||||
// AssetsAdmin contains assets for the 'Admin' entrypoint.
|
||||
var AssetsAdmin = Assets{
|
||||
Scripts: `<script nomodule="" defer src="/static/User.b2f9a57c.js"></script><script type="module" src="/static/User.e0367d79.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.4ca3cb6f.js"></script><script src="/static/Admin.9750ba9c.js" nomodule="" defer></script>`,
|
||||
Scripts: `<script nomodule="" defer src="/static/User.b2f9a57c.js"></script><script type="module" src="/static/User.e0367d79.js"></script><script type="module" src="/static/Default.38d394c2.js"></script><script src="/static/Default.38d394c2.js" nomodule="" defer></script><script type="module" src="/static/Admin.1a380f6f.js"></script><script src="/static/Admin.cb58d290.js" nomodule="" defer></script>`,
|
||||
Styles: `<link rel="stylesheet" href="/static/Default.db26a303.css"><link rel="stylesheet" href="/static/Admin.6d59e220.css"><link rel="stylesheet" href="/static/User.840de3b4.css"><link rel="stylesheet" href="/static/User.68febbf8.css"><link rel="stylesheet" href="/static/Admin.6d2ae968.css">`,
|
||||
}
|
||||
|
|
|
|||
1
internal/dis/component/control/static/dist/Admin.1a380f6f.js
vendored
Normal file
1
internal/dis/component/control/static/dist/Admin.1a380f6f.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
internal/dis/component/control/static/dist/Admin.cb58d290.js
vendored
Normal file
1
internal/dis/component/control/static/dist/Admin.cb58d290.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -49,16 +49,40 @@ function makeTextBuffer(target: HTMLElement, scrollContainer: HTMLElement, size:
|
|||
const remote_action = document.getElementsByClassName('remote-action')
|
||||
Array.from(remote_action).forEach((element) => {
|
||||
const action = element.getAttribute('data-action') as string;
|
||||
const reload = element.hasAttribute('data-force-reload');
|
||||
const reload = element.getAttribute('data-force-reload');
|
||||
const param = element.getAttribute('data-param') as string | undefined;
|
||||
|
||||
const confirmElementName = element.getAttribute('data-confirm-param');
|
||||
const confirmElement = (confirmElementName ? document.querySelector(confirmElementName) : null) as HTMLInputElement | null;
|
||||
|
||||
const bufferSize = (function () {
|
||||
const number = parseInt(element.getAttribute('data-buffer') ?? "", 10) ?? 0;
|
||||
return (isFinite(number) && number > 0) ? number : 0;
|
||||
})()
|
||||
|
||||
const validate = function() {
|
||||
if (!confirmElement) return true
|
||||
return confirmElement.value === param;
|
||||
}
|
||||
|
||||
if (confirmElement) {
|
||||
const runValidation = () => {
|
||||
if (validate()) {
|
||||
element.removeAttribute('disabled')
|
||||
} else {
|
||||
element.setAttribute('disabled', 'disabled')
|
||||
}
|
||||
}
|
||||
confirmElement.addEventListener('change', runValidation)
|
||||
runValidation()
|
||||
}
|
||||
|
||||
element.addEventListener('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// do nothing if the validation fails
|
||||
if (!validate()) return;
|
||||
|
||||
// create a modal dialog and append it to the body
|
||||
const modal = document.createElement("div")
|
||||
modal.className = "modal-terminal"
|
||||
|
|
@ -73,14 +97,18 @@ Array.from(remote_action).forEach((element) => {
|
|||
// create a button to eventually close everything
|
||||
const button = document.createElement("button")
|
||||
button.className = "pure-button pure-button-success"
|
||||
button.append(reload ? "Close & Reload" : "Close")
|
||||
button.append(typeof reload === 'string' ? "Close & Reload" : "Close")
|
||||
button.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (reload) {
|
||||
button.setAttribute('disabled', 'disabled');
|
||||
if (typeof reload === 'string') {
|
||||
button.setAttribute('disabled', 'disabled')
|
||||
target.innerHTML = 'Reloading page ...'
|
||||
location.reload()
|
||||
if (reload === '') {
|
||||
location.reload()
|
||||
} else {
|
||||
location.href = reload
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
86
internal/dis/component/instances/purger/purger.go
Normal file
86
internal/dis/component/instances/purger/purger.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package purger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/pkg/logging"
|
||||
"github.com/tkw1536/goprogram/exit"
|
||||
)
|
||||
|
||||
// Purger purges instances from the distillery
|
||||
type Purger struct {
|
||||
component.Base
|
||||
Dependencies struct {
|
||||
Instances *instances.Instances
|
||||
Provisionable []component.Provisionable
|
||||
}
|
||||
}
|
||||
|
||||
var errPurgeNoDetails = exit.Error{
|
||||
Message: "unable to find instance details for purge: %s",
|
||||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
var errPurgeGeneric = exit.Error{
|
||||
Message: "unable to purge instance %q: %s",
|
||||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
|
||||
// Purge permanently purges an instance from the distillery.
|
||||
// The instance does not have to exist; in which case the resources are also deleted.
|
||||
func (purger *Purger) Purge(ctx context.Context, out io.Writer, slug string) error {
|
||||
logging.LogMessage(out, ctx, "Checking bookkeeping table")
|
||||
instance, err := purger.Dependencies.Instances.WissKI(ctx, slug)
|
||||
if err == instances.ErrWissKINotFound {
|
||||
fmt.Fprintln(out, "Not found in bookkeeping table, assuming defaults")
|
||||
instance, err = purger.Dependencies.Instances.Create(slug)
|
||||
}
|
||||
if err != nil {
|
||||
return errPurgeNoDetails.WithMessageF(err)
|
||||
}
|
||||
|
||||
// remove docker stack
|
||||
logging.LogMessage(out, ctx, "Stopping and removing docker container")
|
||||
if err := instance.Barrel().Stack().Down(ctx, out); err != nil {
|
||||
fmt.Fprintln(out, err)
|
||||
}
|
||||
|
||||
// remove the filesystem
|
||||
logging.LogMessage(out, ctx, "Removing from filesystem %s", instance.FilesystemBase)
|
||||
if err := purger.Environment.RemoveAll(instance.FilesystemBase); err != nil {
|
||||
fmt.Fprintln(out, err)
|
||||
}
|
||||
|
||||
// purge all the instance specific resources
|
||||
if err := logging.LogOperation(func() error {
|
||||
domain := instance.Domain()
|
||||
for _, pc := range purger.Dependencies.Provisionable {
|
||||
logging.LogMessage(out, ctx, "Purging %s resources", pc.Name())
|
||||
err := pc.Purge(ctx, instance.Instance, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, out, ctx, "Purging instance-specific resources"); err != nil {
|
||||
return errPurgeGeneric.WithMessageF(slug, err)
|
||||
}
|
||||
|
||||
// remove from bookkeeping
|
||||
logging.LogMessage(out, ctx, "Removing instance from bookkeeping")
|
||||
if err := instance.Bookkeeping().Delete(ctx); err != nil {
|
||||
fmt.Fprintln(out, err)
|
||||
}
|
||||
|
||||
// remove the filesystem
|
||||
logging.LogMessage(out, ctx, "Remove lock data")
|
||||
if instance.Locker().TryUnlock(ctx) {
|
||||
fmt.Fprintln(out, "instance was not locked")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -14,7 +14,8 @@ type Routeable interface {
|
|||
// Routes returns information about the routes to be handled by this Routeable
|
||||
Routes() Routes
|
||||
|
||||
// HandleRoute returns the handler for the requested path
|
||||
// HandleRoute returns the handler for the requested path.
|
||||
// Context is cancelled once the handler should be closed.
|
||||
HandleRoute(ctx context.Context, path string) (http.Handler, error)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/exporter/logger"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/malt"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances/purger"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/meta"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/resolver"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/solr"
|
||||
|
|
@ -119,6 +120,10 @@ func (dis *Distillery) Custom() *custom.Custom {
|
|||
return export[*custom.Custom](dis)
|
||||
}
|
||||
|
||||
func (dis *Distillery) Purger() *purger.Purger {
|
||||
return export[*purger.Purger](dis)
|
||||
}
|
||||
|
||||
//
|
||||
// All components
|
||||
// THESE SHOULD NEVER BE CALLED DIRECTLY
|
||||
|
|
@ -156,6 +161,9 @@ func (dis *Distillery) allComponents() []initFunc {
|
|||
auto[*meta.Meta],
|
||||
auto[*malt.Malt],
|
||||
|
||||
// Purger
|
||||
auto[*purger.Purger],
|
||||
|
||||
// Snapshots
|
||||
auto[*exporter.Exporter],
|
||||
auto[*logger.Logger],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue