diff --git a/cmd/purge.go b/cmd/purge.go index c94de7a..f575381 100644 --- a/cmd/purge.go +++ b/cmd/purge.go @@ -3,8 +3,6 @@ package cmd import ( wisski_distillery "github.com/FAU-CDI/wisski-distillery" "github.com/FAU-CDI/wisski-distillery/internal/cli" - "github.com/FAU-CDI/wisski-distillery/internal/dis/component/instances" - "github.com/FAU-CDI/wisski-distillery/pkg/logging" "github.com/tkw1536/goprogram/exit" ) @@ -28,21 +26,11 @@ func (purge) Description() wisski_distillery.Description { } } -var errPurgeNoDetails = exit.Error{ - Message: "unable to find instance details for purge: %s", - ExitCode: exit.ExitGeneric, -} - var errPurgeNoConfirmation = exit.Error{ Message: "aborting after request was not confirmed. either type `yes` or pass `--yes` on the command line", ExitCode: exit.ExitGeneric, } -var errPurgeGeneric = exit.Error{ - Message: "unable to purge instance %q: %s", - ExitCode: exit.ExitGeneric, -} - func (p purge) Run(context wisski_distillery.Context) error { dis := context.Environment slug := p.Positionals.Slug @@ -57,57 +45,5 @@ func (p purge) Run(context wisski_distillery.Context) error { } } - // load the instance (first via bookkeeping, then via defaults) - logging.LogMessage(context.Stderr, context.Context, "Checking bookkeeping table") - instance, err := dis.Instances().WissKI(context.Context, slug) - if err == instances.ErrWissKINotFound { - context.Println("Not found in bookkeeping table, assuming defaults") - instance, err = dis.Instances().Create(slug) - } - if err != nil { - return errPurgeNoDetails.WithMessageF(err) - } - - // remove docker stack - logging.LogMessage(context.Stderr, context.Context, "Stopping and removing docker container") - if err := instance.Barrel().Stack().Down(context.Context, context.Stderr); err != nil { - context.EPrintln(err) - } - - // remove the filesystem - logging.LogMessage(context.Stderr, context.Context, "Removing from filesystem %s", instance.FilesystemBase) - if err := dis.Still.Environment.RemoveAll(instance.FilesystemBase); err != nil { - context.EPrintln(err) - } - - // purge all the instance specific resources - if err := logging.LogOperation(func() error { - domain := instance.Domain() - for _, pc := range dis.Provisionable() { - logging.LogMessage(context.Stderr, context.Context, "Purging %s resources", pc.Name()) - err := pc.Purge(context.Context, instance.Instance, domain) - if err != nil { - return err - } - } - - return nil - }, context.Stderr, context.Context, "Purging instance-specific resources"); err != nil { - return errPurgeGeneric.WithMessageF(slug, err) - } - - // remove from bookkeeping - logging.LogMessage(context.Stderr, context.Context, "Removing instance from bookkeeping") - if err := instance.Bookkeeping().Delete(context.Context); err != nil { - context.EPrintln(err) - } - - // remove the filesystem - logging.LogMessage(context.Stderr, context.Context, "Remove lock data") - if instance.Locker().TryUnlock(context.Context) { - context.EPrintln("instance was not locked") - } - - logging.LogMessage(context.Stderr, context.Context, "Instance %s has been purged", slug) - return nil + return dis.Purger().Purge(context.Context, context.Stdout, slug) } diff --git a/internal/cli/cli_notices.go b/internal/cli/cli_notices.go index 2bf8490..b6c986e 100755 --- a/internal/cli/cli_notices.go +++ b/internal/cli/cli_notices.go @@ -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() { diff --git a/internal/dis/component/control/admin/admin.go b/internal/dis/component/control/admin/admin.go index 2a70807..06b80b8 100644 --- a/internal/dis/component/control/admin/admin.go +++ b/internal/dis/component/control/admin/admin.go @@ -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 diff --git a/internal/dis/component/control/admin/html/instance.html b/internal/dis/component/control/admin/html/instance.html index 633d0fa..5bbf630 100644 --- a/internal/dis/component/control/admin/html/instance.html +++ b/internal/dis/component/control/admin/html/instance.html @@ -40,10 +40,10 @@ {{ .Info.Running }}
- -
@@ -144,7 +144,7 @@ Last Rebuild
- + {{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }} @@ -153,7 +153,7 @@ Last Cron
- + {{ .Info.LastRebuild.Format "2006-01-02T15:04:05Z07:00" }} @@ -162,7 +162,7 @@ Last Update
- + {{ .Info.LastUpdate.Format "2006-01-02T15:04:05Z07:00" }}
@@ -437,7 +437,7 @@

Snapshots

- +

@@ -467,4 +467,22 @@ + +
+

Dangerous Actions

+
+ +
+

+ 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 {{ .Instance.Slug }} to confirm purging. +

+
+
+ + +
+
+
{{ end }} \ No newline at end of file diff --git a/internal/dis/component/control/admin/socket.go b/internal/dis/component/control/admin/socket.go index f858f4f..6fe1b4b 100644 --- a/internal/dis/component/control/admin/socket.go +++ b/internal/dis/component/control/admin/socket.go @@ -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) { diff --git a/internal/dis/component/control/news/NEWS/2023-01-15-news.md b/internal/dis/component/control/news/NEWS/2023-01-15-ssh.md similarity index 100% rename from internal/dis/component/control/news/NEWS/2023-01-15-news.md rename to internal/dis/component/control/news/NEWS/2023-01-15-ssh.md diff --git a/internal/dis/component/control/news/NEWS/2023-01-16-remove.md b/internal/dis/component/control/news/NEWS/2023-01-16-remove.md new file mode 100644 index 0000000..a8f93a7 --- /dev/null +++ b/internal/dis/component/control/news/NEWS/2023-01-16-remove.md @@ -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 \ No newline at end of file diff --git a/internal/dis/component/control/static/assets_dist.go b/internal/dis/component/control/static/assets_dist.go index e38a4ca..215b4c3 100644 --- a/internal/dis/component/control/static/assets_dist.go +++ b/internal/dis/component/control/static/assets_dist.go @@ -21,6 +21,6 @@ var AssetsUser = Assets{ // AssetsAdmin contains assets for the 'Admin' entrypoint. var AssetsAdmin = Assets{ - Scripts: ``, + Scripts: ``, Styles: ``, } diff --git a/internal/dis/component/control/static/dist/Admin.1a380f6f.js b/internal/dis/component/control/static/dist/Admin.1a380f6f.js new file mode 100644 index 0000000..dca5b97 --- /dev/null +++ b/internal/dis/component/control/static/dist/Admin.1a380f6f.js @@ -0,0 +1 @@ +function t(t){return t&&t.__esModule?t.default:t}var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},i=e.parcelRequireafa4;function s(t,e){return new Promise(((n,r)=>{const i=new WebSocket(location.href.replace("http","ws"));i.onclose=n,i.onerror=r,i.onmessage=t=>e(t.data),i.onopen=()=>t(i)}))}null==i&&((i=function(t){if(t in n)return n[t].exports;if(t in r){var e=r[t];delete r[t];var i={id:t,exports:{}};return n[t]=i,e.call(i.exports,i,i.exports),i.exports}var s=new Error("Cannot find module '"+t+"'");throw s.code="MODULE_NOT_FOUND",s}).register=function(t,e){r[t]=e},e.parcelRequireafa4=i);const o=document.getElementsByClassName("remote-action");Array.from(o).forEach((t=>{const e=t.getAttribute("data-action"),n=t.getAttribute("data-force-reload"),r=t.getAttribute("data-param"),i=t.getAttribute("data-confirm-param"),o=i?document.querySelector(i):null,a=function(){const e=parseInt(t.getAttribute("data-buffer")??"",10)??0;return isFinite(e)&&e>0?e:0}(),u=function(){return!o||o.value===r};if(o){const e=()=>{u()?t.removeAttribute("disabled"):t.setAttribute("disabled","disabled")};o.addEventListener("change",e),e()}t.addEventListener("click",(function(t){if(t.preventDefault(),!u())return;const i=document.createElement("div");i.className="modal-terminal",document.body.append(i);const o=document.createElement("pre"),c=function(t,e,n){let r=null;const i=[],s=()=>{o.paintedFrames++,t.innerText=i.join("\n"),e.scrollTop=e.scrollHeight,r=null},o=(t,e)=>{if(i.push(t),0!==n&&i.length>n&&i.splice(0,i.length-n),null!==r&&(o.missedFrames++,window.cancelAnimationFrame(r)),e)return s();r=window.requestAnimationFrame(s)};return o.paintedFrames=0,o.missedFrames=0,o}(o,i,a);i.append(o);const d=document.createElement("button");d.className="pure-button pure-button-success",d.append("string"==typeof n?"Close & Reload":"Close"),d.addEventListener("click",(function(t){if(t.preventDefault(),"string"==typeof n)return d.setAttribute("disabled","disabled"),o.innerHTML="Reloading page ...",void(""===n?location.reload():location.href=n);i.parentNode?.removeChild(i)}));const l=window.onbeforeunload;window.onbeforeunload=()=>"A remote session is in progress. Are you sure you want to leave?";let f=!1;const h=function(){f||(f=!0,window.onbeforeunload=l,i.append(d))};c("Connecting ...",!0),s((t=>{c("Connected",!0),t.send(e),"string"==typeof r&&t.send(r)}),(t=>{c(t)})).then((()=>{c("Connection closed.",!0),h()})).catch((()=>{c("Connection errored.",!0),h()}))}))}));const a=document.getElementsByClassName("remote-link");Array.from(a).forEach((t=>{const e=t.getAttribute("data-action"),n=t.getAttribute("data-params")?.split(" ");t.addEventListener("click",(function(t){t.preventDefault(),async function(t,e){return new Promise(((n,r)=>{let i="";var o=function(){const t=i.indexOf("\n");if(t<0)return void r("invalid buffer");if(!("true"===i.substring(0,t)))return void r(i);const e=JSON.parse(i.substring(t+1));n(e)};s((n=>{n.send(t),e&&e.forEach((t=>n.send(t)))}),(t=>{i+=t+"\n"})).then((()=>{o()})).catch((()=>{i="false\n",o()}))}))}(e,n).then((t=>{window.open(t)})).catch((t=>{console.error(t)}))}))}));var u={};u=function(){var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",o="hour",a="day",u="week",c="month",d="quarter",l="year",f="date",h="Invalid Date",m=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,p=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,$={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},g=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},y={s:g,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+g(r,2,"0")+":"+g(i,2,"0")},m:function t(e,n){if(e.date()1)return t(o[0])}else{var a=e.name;w[a]=e,i=a}return!r&&i&&(v=i),i||!r&&v},D=function(t,e){if(b(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new O(n)},S=y;S.l=M,S.i=b,S.w=function(t,e){return D(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var O=function(){function $(t){this.$L=M(t.locale,null,!0),this.parse(t)}var g=$.prototype;return g.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(S.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(m);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},g.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},g.$utils=function(){return S},g.isValid=function(){return!(this.$d.toString()===h)},g.isSame=function(t,e){var n=D(t);return this.startOf(e)<=n&&n<=this.endOf(e)},g.isAfter=function(t,e){return D(t){const n=t(u)(e.innerText),r=n.format("YYYY-MM-DD HH:mm:ss ([UTC]Z)");if(0===n.unix()){const t=document.createElement("code");return t.style.color="gray",t.append(r),t}return r},path:t=>{const e=t.innerText.split("/");return e[e.length-1]},pathbuilder:t=>{const e=(t.getAttribute("data-name")??"pathbuilder")+".xml",[n,r]=d(e,t.innerText,"application/xml");n.className="pure-button";const i=e+" ("+r.size+" Bytes)";return n.append(i),n}},d=(t,e,n)=>{const r=new Blob([e],{type:n??"text/plain"}),i=document.createElement("a");return i.target="_blank",i.download=t,i.href=URL.createObjectURL(r),[i,r]};Object.keys(c).forEach((t=>{const e=c[t];document.querySelectorAll("code."+t).forEach((t=>{const n=e(t);if("string"==typeof n)return t.innerHTML="",void t.appendChild(document.createTextNode(n));t.parentNode.replaceChild(n,t)}))})),i("gkpdw"); \ No newline at end of file diff --git a/internal/dis/component/control/static/dist/Admin.4ca3cb6f.js b/internal/dis/component/control/static/dist/Admin.4ca3cb6f.js deleted file mode 100644 index 51805d4..0000000 --- a/internal/dis/component/control/static/dist/Admin.4ca3cb6f.js +++ /dev/null @@ -1 +0,0 @@ -function t(t){return t&&t.__esModule?t.default:t}var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},i=e.parcelRequireafa4;function s(t,e){return new Promise(((n,r)=>{const i=new WebSocket(location.href.replace("http","ws"));i.onclose=n,i.onerror=r,i.onmessage=t=>e(t.data),i.onopen=()=>t(i)}))}null==i&&((i=function(t){if(t in n)return n[t].exports;if(t in r){var e=r[t];delete r[t];var i={id:t,exports:{}};return n[t]=i,e.call(i.exports,i,i.exports),i.exports}var s=new Error("Cannot find module '"+t+"'");throw s.code="MODULE_NOT_FOUND",s}).register=function(t,e){r[t]=e},e.parcelRequireafa4=i);const o=document.getElementsByClassName("remote-action");Array.from(o).forEach((t=>{const e=t.getAttribute("data-action"),n=t.hasAttribute("data-force-reload"),r=t.getAttribute("data-param"),i=function(){const e=parseInt(t.getAttribute("data-buffer")??"",10)??0;return isFinite(e)&&e>0?e:0}();t.addEventListener("click",(function(t){t.preventDefault();const o=document.createElement("div");o.className="modal-terminal",document.body.append(o);const a=document.createElement("pre"),u=function(t,e,n){let r=null;const i=[],s=()=>{o.paintedFrames++,t.innerText=i.join("\n"),e.scrollTop=e.scrollHeight,r=null},o=(t,e)=>{if(i.push(t),0!==n&&i.length>n&&i.splice(0,i.length-n),null!==r&&(o.missedFrames++,window.cancelAnimationFrame(r)),e)return s();r=window.requestAnimationFrame(s)};return o.paintedFrames=0,o.missedFrames=0,o}(a,o,i);o.append(a);const c=document.createElement("button");c.className="pure-button pure-button-success",c.append(n?"Close & Reload":"Close"),c.addEventListener("click",(function(t){if(t.preventDefault(),n)return c.setAttribute("disabled","disabled"),a.innerHTML="Reloading page ...",void location.reload();o.parentNode?.removeChild(o)}));const d=window.onbeforeunload;window.onbeforeunload=()=>"A remote session is in progress. Are you sure you want to leave?";let l=!1;const f=function(){l||(l=!0,window.onbeforeunload=d,o.append(c))};u("Connecting ...",!0),s((t=>{u("Connected",!0),t.send(e),"string"==typeof r&&t.send(r)}),(t=>{u(t)})).then((()=>{u("Connection closed.",!0),f()})).catch((()=>{u("Connection errored.",!0),f()}))}))}));const a=document.getElementsByClassName("remote-link");Array.from(a).forEach((t=>{const e=t.getAttribute("data-action"),n=t.getAttribute("data-params")?.split(" ");t.addEventListener("click",(function(t){t.preventDefault(),async function(t,e){return new Promise(((n,r)=>{let i="";var o=function(){const t=i.indexOf("\n");if(t<0)return void r("invalid buffer");if(!("true"===i.substring(0,t)))return void r(i);const e=JSON.parse(i.substring(t+1));n(e)};s((n=>{n.send(t),e&&e.forEach((t=>n.send(t)))}),(t=>{i+=t+"\n"})).then((()=>{o()})).catch((()=>{i="false\n",o()}))}))}(e,n).then((t=>{window.open(t)})).catch((t=>{console.error(t)}))}))}));var u={};u=function(){var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",o="hour",a="day",u="week",c="month",d="quarter",l="year",f="date",h="Invalid Date",m=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,p=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,$={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},g=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},y={s:g,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+g(r,2,"0")+":"+g(i,2,"0")},m:function t(e,n){if(e.date()1)return t(o[0])}else{var a=e.name;w[a]=e,i=a}return!r&&i&&(v=i),i||!r&&v},b=function(t,e){if(M(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new O(n)},S=y;S.l=D,S.i=M,S.w=function(t,e){return b(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var O=function(){function $(t){this.$L=D(t.locale,null,!0),this.parse(t)}var g=$.prototype;return g.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(S.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(m);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},g.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},g.$utils=function(){return S},g.isValid=function(){return!(this.$d.toString()===h)},g.isSame=function(t,e){var n=b(t);return this.startOf(e)<=n&&n<=this.endOf(e)},g.isAfter=function(t,e){return b(t){const n=t(u)(e.innerText),r=n.format("YYYY-MM-DD HH:mm:ss ([UTC]Z)");if(0===n.unix()){const t=document.createElement("code");return t.style.color="gray",t.append(r),t}return r},path:t=>{const e=t.innerText.split("/");return e[e.length-1]},pathbuilder:t=>{const e=(t.getAttribute("data-name")??"pathbuilder")+".xml",[n,r]=d(e,t.innerText,"application/xml");n.className="pure-button";const i=e+" ("+r.size+" Bytes)";return n.append(i),n}},d=(t,e,n)=>{const r=new Blob([e],{type:n??"text/plain"}),i=document.createElement("a");return i.target="_blank",i.download=t,i.href=URL.createObjectURL(r),[i,r]};Object.keys(c).forEach((t=>{const e=c[t];document.querySelectorAll("code."+t).forEach((t=>{const n=e(t);if("string"==typeof n)return t.innerHTML="",void t.appendChild(document.createTextNode(n));t.parentNode.replaceChild(n,t)}))})),i("gkpdw"); \ No newline at end of file diff --git a/internal/dis/component/control/static/dist/Admin.9750ba9c.js b/internal/dis/component/control/static/dist/Admin.9750ba9c.js deleted file mode 100644 index bdac3e1..0000000 --- a/internal/dis/component/control/static/dist/Admin.9750ba9c.js +++ /dev/null @@ -1 +0,0 @@ -!function(){function t(t){return t&&t.__esModule?t.default:t}var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},i=e.parcelRequireafa4;function s(t,e){return new Promise(((n,r)=>{const i=new WebSocket(location.href.replace("http","ws"));i.onclose=n,i.onerror=r,i.onmessage=t=>e(t.data),i.onopen=()=>t(i)}))}null==i&&((i=function(t){if(t in n)return n[t].exports;if(t in r){var e=r[t];delete r[t];var i={id:t,exports:{}};return n[t]=i,e.call(i.exports,i,i.exports),i.exports}var s=new Error("Cannot find module '"+t+"'");throw s.code="MODULE_NOT_FOUND",s}).register=function(t,e){r[t]=e},e.parcelRequireafa4=i);const o=document.getElementsByClassName("remote-action");Array.from(o).forEach((t=>{const e=t.getAttribute("data-action"),n=t.hasAttribute("data-force-reload"),r=t.getAttribute("data-param"),i=function(){var e,n;const r=null!==(n=parseInt(null!==(e=t.getAttribute("data-buffer"))&&void 0!==e?e:"",10))&&void 0!==n?n:0;return isFinite(r)&&r>0?r:0}();t.addEventListener("click",(function(t){t.preventDefault();const o=document.createElement("div");o.className="modal-terminal",document.body.append(o);const a=document.createElement("pre"),u=function(t,e,n){let r=null;const i=[],s=()=>{o.paintedFrames++,t.innerText=i.join("\n"),e.scrollTop=e.scrollHeight,r=null},o=(t,e)=>{if(i.push(t),0!==n&&i.length>n&&i.splice(0,i.length-n),null!==r&&(o.missedFrames++,window.cancelAnimationFrame(r)),e)return s();r=window.requestAnimationFrame(s)};return o.paintedFrames=0,o.missedFrames=0,o}(a,o,i);o.append(a);const c=document.createElement("button");c.className="pure-button pure-button-success",c.append(n?"Close & Reload":"Close"),c.addEventListener("click",(function(t){var e;if(t.preventDefault(),n)return c.setAttribute("disabled","disabled"),a.innerHTML="Reloading page ...",void location.reload();null===(e=o.parentNode)||void 0===e||e.removeChild(o)}));const d=window.onbeforeunload;window.onbeforeunload=()=>"A remote session is in progress. Are you sure you want to leave?";let l=!1;const f=function(){l||(l=!0,window.onbeforeunload=d,o.append(c))};u("Connecting ...",!0),s((t=>{u("Connected",!0),t.send(e),"string"==typeof r&&t.send(r)}),(t=>{u(t)})).then((()=>{u("Connection closed.",!0),f()})).catch((()=>{u("Connection errored.",!0),f()}))}))}));const a=document.getElementsByClassName("remote-link");Array.from(a).forEach((t=>{const e=t.getAttribute("data-action"),n=t.getAttribute("data-params"),r=null==n?void 0:n.split(" ");t.addEventListener("click",(function(t){t.preventDefault(),async function(t,e){return new Promise(((n,r)=>{let i="";var o=function(){const t=i.indexOf("\n");if(t<0)return void r("invalid buffer");if(!("true"===i.substring(0,t)))return void r(i);const e=JSON.parse(i.substring(t+1));n(e)};s((n=>{n.send(t),e&&e.forEach((t=>n.send(t)))}),(t=>{i+=t+"\n"})).then((()=>{o()})).catch((()=>{i="false\n",o()}))}))}(e,r).then((t=>{window.open(t)})).catch((t=>{console.error(t)}))}))}));var u={};u=function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",o="hour",a="day",u="week",c="month",d="quarter",l="year",f="date",h="Invalid Date",m=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,p=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,$={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},v=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},g={s:v,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+v(r,2,"0")+":"+v(i,2,"0")},m:function t(e,n){if(e.date()1)return t(o[0])}else{var a=e.name;w[a]=e,i=a}return!r&&i&&(y=i),i||!r&&y},b=function(t,e){if(M(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new O(n)},S=g;S.l=D,S.i=M,S.w=function(t,e){return b(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var O=function(){function $(t){this.$L=D(t.locale,null,!0),this.parse(t)}var v=$.prototype;return v.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(S.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(m);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},v.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},v.$utils=function(){return S},v.isValid=function(){return!(this.$d.toString()===h)},v.isSame=function(t,e){var n=b(t);return this.startOf(e)<=n&&n<=this.endOf(e)},v.isAfter=function(t,e){return b(t){const n=t(u)(e.innerText),r=n.format("YYYY-MM-DD HH:mm:ss ([UTC]Z)");if(0===n.unix()){const t=document.createElement("code");return t.style.color="gray",t.append(r),t}return r},path:t=>{const e=t.innerText.split("/");return e[e.length-1]},pathbuilder:t=>{var e;const n=(null!==(e=t.getAttribute("data-name"))&&void 0!==e?e:"pathbuilder")+".xml",[r,i]=d(n,t.innerText,"application/xml");r.className="pure-button";const s=n+" ("+i.size+" Bytes)";return r.append(s),r}},d=(t,e,n)=>{const r=new Blob([e],{type:null!=n?n:"text/plain"}),i=document.createElement("a");return i.target="_blank",i.download=t,i.href=URL.createObjectURL(r),[i,r]};Object.keys(c).forEach((t=>{const e=c[t];document.querySelectorAll("code."+t).forEach((t=>{const n=e(t);if("string"==typeof n)return t.innerHTML="",void t.appendChild(document.createTextNode(n));t.parentNode.replaceChild(n,t)}))})),i("kEAtK")}(); \ No newline at end of file diff --git a/internal/dis/component/control/static/dist/Admin.cb58d290.js b/internal/dis/component/control/static/dist/Admin.cb58d290.js new file mode 100644 index 0000000..4dfac03 --- /dev/null +++ b/internal/dis/component/control/static/dist/Admin.cb58d290.js @@ -0,0 +1 @@ +!function(){function t(t){return t&&t.__esModule?t.default:t}var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},n={},r={},i=e.parcelRequireafa4;function s(t,e){return new Promise(((n,r)=>{const i=new WebSocket(location.href.replace("http","ws"));i.onclose=n,i.onerror=r,i.onmessage=t=>e(t.data),i.onopen=()=>t(i)}))}null==i&&((i=function(t){if(t in n)return n[t].exports;if(t in r){var e=r[t];delete r[t];var i={id:t,exports:{}};return n[t]=i,e.call(i.exports,i,i.exports),i.exports}var s=new Error("Cannot find module '"+t+"'");throw s.code="MODULE_NOT_FOUND",s}).register=function(t,e){r[t]=e},e.parcelRequireafa4=i);const o=document.getElementsByClassName("remote-action");Array.from(o).forEach((t=>{const e=t.getAttribute("data-action"),n=t.getAttribute("data-force-reload"),r=t.getAttribute("data-param"),i=t.getAttribute("data-confirm-param"),o=i?document.querySelector(i):null,a=function(){var e,n;const r=null!==(n=parseInt(null!==(e=t.getAttribute("data-buffer"))&&void 0!==e?e:"",10))&&void 0!==n?n:0;return isFinite(r)&&r>0?r:0}(),u=function(){return!o||o.value===r};if(o){const e=()=>{u()?t.removeAttribute("disabled"):t.setAttribute("disabled","disabled")};o.addEventListener("change",e),e()}t.addEventListener("click",(function(t){if(t.preventDefault(),!u())return;const i=document.createElement("div");i.className="modal-terminal",document.body.append(i);const o=document.createElement("pre"),c=function(t,e,n){let r=null;const i=[],s=()=>{o.paintedFrames++,t.innerText=i.join("\n"),e.scrollTop=e.scrollHeight,r=null},o=(t,e)=>{if(i.push(t),0!==n&&i.length>n&&i.splice(0,i.length-n),null!==r&&(o.missedFrames++,window.cancelAnimationFrame(r)),e)return s();r=window.requestAnimationFrame(s)};return o.paintedFrames=0,o.missedFrames=0,o}(o,i,a);i.append(o);const d=document.createElement("button");d.className="pure-button pure-button-success",d.append("string"==typeof n?"Close & Reload":"Close"),d.addEventListener("click",(function(t){var e;if(t.preventDefault(),"string"==typeof n)return d.setAttribute("disabled","disabled"),o.innerHTML="Reloading page ...",void(""===n?location.reload():location.href=n);null===(e=i.parentNode)||void 0===e||e.removeChild(i)}));const l=window.onbeforeunload;window.onbeforeunload=()=>"A remote session is in progress. Are you sure you want to leave?";let f=!1;const h=function(){f||(f=!0,window.onbeforeunload=l,i.append(d))};c("Connecting ...",!0),s((t=>{c("Connected",!0),t.send(e),"string"==typeof r&&t.send(r)}),(t=>{c(t)})).then((()=>{c("Connection closed.",!0),h()})).catch((()=>{c("Connection errored.",!0),h()}))}))}));const a=document.getElementsByClassName("remote-link");Array.from(a).forEach((t=>{const e=t.getAttribute("data-action"),n=t.getAttribute("data-params"),r=null==n?void 0:n.split(" ");t.addEventListener("click",(function(t){t.preventDefault(),async function(t,e){return new Promise(((n,r)=>{let i="";var o=function(){const t=i.indexOf("\n");if(t<0)return void r("invalid buffer");if(!("true"===i.substring(0,t)))return void r(i);const e=JSON.parse(i.substring(t+1));n(e)};s((n=>{n.send(t),e&&e.forEach((t=>n.send(t)))}),(t=>{i+=t+"\n"})).then((()=>{o()})).catch((()=>{i="false\n",o()}))}))}(e,r).then((t=>{window.open(t)})).catch((t=>{console.error(t)}))}))}));var u={};u=function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",o="hour",a="day",u="week",c="month",d="quarter",l="year",f="date",h="Invalid Date",m=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,p=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,$={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},g=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:g,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+g(r,2,"0")+":"+g(i,2,"0")},m:function t(e,n){if(e.date()1)return t(o[0])}else{var a=e.name;w[a]=e,i=a}return!r&&i&&(y=i),i||!r&&y},D=function(t,e){if(b(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new O(n)},S=v;S.l=M,S.i=b,S.w=function(t,e){return D(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var O=function(){function $(t){this.$L=M(t.locale,null,!0),this.parse(t)}var g=$.prototype;return g.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(S.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(m);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},g.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},g.$utils=function(){return S},g.isValid=function(){return!(this.$d.toString()===h)},g.isSame=function(t,e){var n=D(t);return this.startOf(e)<=n&&n<=this.endOf(e)},g.isAfter=function(t,e){return D(t){const n=t(u)(e.innerText),r=n.format("YYYY-MM-DD HH:mm:ss ([UTC]Z)");if(0===n.unix()){const t=document.createElement("code");return t.style.color="gray",t.append(r),t}return r},path:t=>{const e=t.innerText.split("/");return e[e.length-1]},pathbuilder:t=>{var e;const n=(null!==(e=t.getAttribute("data-name"))&&void 0!==e?e:"pathbuilder")+".xml",[r,i]=d(n,t.innerText,"application/xml");r.className="pure-button";const s=n+" ("+i.size+" Bytes)";return r.append(s),r}},d=(t,e,n)=>{const r=new Blob([e],{type:null!=n?n:"text/plain"}),i=document.createElement("a");return i.target="_blank",i.download=t,i.href=URL.createObjectURL(r),[i,r]};Object.keys(c).forEach((t=>{const e=c[t];document.querySelectorAll("code."+t).forEach((t=>{const n=e(t);if("string"==typeof n)return t.innerHTML="",void t.appendChild(document.createTextNode(n));t.parentNode.replaceChild(n,t)}))})),i("kEAtK")}(); \ No newline at end of file diff --git a/internal/dis/component/control/static/src/lib/remote/index.ts b/internal/dis/component/control/static/src/lib/remote/index.ts index 5898820..c09a6b9 100644 --- a/internal/dis/component/control/static/src/lib/remote/index.ts +++ b/internal/dis/component/control/static/src/lib/remote/index.ts @@ -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; } diff --git a/internal/dis/component/instances/purger/purger.go b/internal/dis/component/instances/purger/purger.go new file mode 100644 index 0000000..dd5800f --- /dev/null +++ b/internal/dis/component/instances/purger/purger.go @@ -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 +} diff --git a/internal/dis/component/server.go b/internal/dis/component/server.go index a4f8aff..aa4e583 100644 --- a/internal/dis/component/server.go +++ b/internal/dis/component/server.go @@ -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) } diff --git a/internal/dis/distillery.go b/internal/dis/distillery.go index ae431c4..f57104a 100644 --- a/internal/dis/distillery.go +++ b/internal/dis/distillery.go @@ -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],