+ 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],