diff --git a/docs/managing-a-cluster.md b/docs/managing-a-cluster.md index 54dc1ce8..06483749 100644 --- a/docs/managing-a-cluster.md +++ b/docs/managing-a-cluster.md @@ -43,6 +43,14 @@ The controller also supports remote updates, following the same logic. The corre ctl> controller update [--restart] controller> config set controller.allow_remote_updates true/false +### Mark restart required + + ctl> host mark-restart-required + +Marks the host as requiring a restart, the same way a plugin update does. Use it after installing or changing a plugin by other means than `host plugin install`, such as a plugin checked out into `external_plugins`. The mark shows up in the web interface on the next system metrics poll. The controller has the same command: + + ctl> controller mark-restart-required + ## Instances ### List Instances diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 091b7d01..f715bf1f 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -73,6 +73,9 @@ export default class ControlConnection extends BaseConnection { this.handle(lib.ControllerStopRequest, this.handleControllerStopRequest.bind(this)); this.handle(lib.ControllerRestartRequest, this.handleControllerRestartRequest.bind(this)); + this.handle( + lib.ControllerMarkRestartRequiredRequest, this.handleControllerMarkRestartRequiredRequest.bind(this) + ); this.handle(lib.ControllerUpdateRequest, this.handleControllerUpdateRequest.bind(this)); this.handle(lib.ControllerConfigGetRequest, this.handleControllerConfigGetRequest.bind(this)); this.handle(lib.ControllerConfigSetRequest, this.handleControllerConfigSetRequest.bind(this)); @@ -1297,6 +1300,10 @@ export default class ControlConnection extends BaseConnection { return usersToSend; } + async handleControllerMarkRestartRequiredRequest() { + this._controller.config.restartRequired = true; + } + async handleControllerUpdateRequest(request: lib.ControllerUpdateRequest) { if (!this._controller.config.get("controller.allow_remote_updates")) { throw new lib.RequestError("Remote updates are disabled on this machine"); diff --git a/packages/ctl/src/commands_controller.ts b/packages/ctl/src/commands_controller.ts index 266fadb9..ad4a31cf 100644 --- a/packages/ctl/src/commands_controller.ts +++ b/packages/ctl/src/commands_controller.ts @@ -25,6 +25,12 @@ controllerCommands.add(new lib.Command({ await ctl.send(new lib.ControllerRestartRequest()); }, })); +controllerCommands.add(new lib.Command({ + definition: ["mark-restart-required", "Mark the controller as requiring a restart"], + handler: async function(_args: object, ctl: Ctl) { + await ctl.send(new lib.ControllerMarkRestartRequiredRequest()); + }, +})); controllerCommands.add(new lib.Command({ definition: ["update", "Update the controller", (yargs) => { yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" }); diff --git a/packages/ctl/src/commands_host.ts b/packages/ctl/src/commands_host.ts index 8661dbe2..1f343181 100644 --- a/packages/ctl/src/commands_host.ts +++ b/packages/ctl/src/commands_host.ts @@ -33,6 +33,16 @@ hostCommands.add(new lib.Command({ }, })); +hostCommands.add(new lib.Command({ + definition: ["mark-restart-required ", "Mark the given host as requiring a restart", (yargs) => { + yargs.positional("host", { describe: "Host to mark", type: "string" }); + }], + handler: async function(args: { host: string }, ctl: Ctl) { + let hostId = await lib.resolveHost(ctl, args.host); + await ctl.sendTo({ hostId }, new lib.HostMarkRestartRequiredRequest()); + }, +})); + hostCommands.add(new lib.Command({ definition: [["list", "l"], "List hosts connected to the controller"], handler: async function(args: object, ctl: Ctl) { diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index a586a982..af99c2ca 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -390,6 +390,7 @@ export default class Host extends lib.Link { this.handle(lib.HostStopRequest, this.handleHostStopRequest.bind(this)); this.handle(lib.HostRestartRequest, this.handleHostRestartRequest.bind(this)); + this.handle(lib.HostMarkRestartRequiredRequest, this.handleHostMarkRestartRequiredRequest.bind(this)); this.handle(lib.HostUpdateRequest, this.handleHostUpdateRequest.bind(this)); this.handle(lib.HostConfigGetRequest, this.handleHostConfigGetRequest.bind(this)); this.handle(lib.HostConfigSetRequest, this.handleHostConfigSetRequest.bind(this)); @@ -511,6 +512,10 @@ export default class Host extends lib.Link { this.shutdown(); } + async handleHostMarkRestartRequiredRequest() { + this.config.restartRequired = true; + } + async handleHostConfigGetRequest() { return this.config.toRemote("control"); } diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index 25ca7789..732ff875 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -20,6 +20,14 @@ export class ControllerRestartRequest { static permission = "core.controller.restart" as const; } +export class ControllerMarkRestartRequiredRequest { + declare ["constructor"]: typeof ControllerMarkRestartRequiredRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.controller.restart" as const; +} + export class ControllerUpdateRequest { declare ["constructor"]: typeof ControllerUpdateRequest; static type = "request" as const; diff --git a/packages/lib/src/data/messages_host.ts b/packages/lib/src/data/messages_host.ts index aa4edbc9..41e8e107 100644 --- a/packages/lib/src/data/messages_host.ts +++ b/packages/lib/src/data/messages_host.ts @@ -20,6 +20,14 @@ export class HostRestartRequest { static permission = "core.host.restart" as const; } +export class HostMarkRestartRequiredRequest { + declare ["constructor"]: typeof HostMarkRestartRequiredRequest; + static type = "request" as const; + static src = ["control", "controller"] as const; + static dst = "host" as const; + static permission = "core.host.restart" as const; +} + export class HostUpdateRequest { declare ["constructor"]: typeof HostUpdateRequest; static type = "request" as const; diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index b3c856d5..448e4780 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -21,6 +21,7 @@ export const dataClasses: (RequestClass | EventClass) controller.ControllerStopRequest, controller.ControllerRestartRequest, + controller.ControllerMarkRestartRequiredRequest, controller.ControllerUpdateRequest, controller.ControllerConfigGetRequest, controller.ControllerConfigSetRequest, @@ -43,6 +44,7 @@ export const dataClasses: (RequestClass | EventClass) host.HostStopRequest, host.HostRestartRequest, + host.HostMarkRestartRequiredRequest, host.HostUpdateRequest, host.HostConfigGetRequest, host.HostConfigSetRequest, diff --git a/packages/web_ui/src/components/ControllerPage.tsx b/packages/web_ui/src/components/ControllerPage.tsx index 1c74cbc9..a6ffabe0 100644 --- a/packages/web_ui/src/components/ControllerPage.tsx +++ b/packages/web_ui/src/components/ControllerPage.tsx @@ -16,7 +16,7 @@ import ControlContext from "./ControlContext"; import ControllerConfigTree from "./ControllerConfigTree"; import PageHeader from "./PageHeader"; import PageLayout from "./PageLayout"; -import { notifyErrorHandler } from "../util/notify"; +import notify, { notifyErrorHandler } from "../util/notify"; import webUiPackage from "../../package.json"; import { hasNpmButtonPermission, NpmButton } from "./NpmButton"; import VariableDropdownButton, { VariableDropdownButtonProps } from "./VariableDropdownButton"; @@ -47,6 +47,20 @@ function ControllerControlButton({ canRestart, restartRequired }: ControllerCont }); } + if (account.hasPermission("core.controller.restart") && !restartRequired) { + actions.push({ + key: "mark-restart-required", + label: "Mark Restart Required", + onClick: () => { + control.send( + new lib.ControllerMarkRestartRequiredRequest() + ).then(() => { + notify("Controller marked as requiring a restart", "success"); + }).catch(notifyErrorHandler("Error marking controller")); + }, + }); + } + if (account.hasPermission("core.controller.stop")) { actions.push({ key: "stop", diff --git a/packages/web_ui/src/components/HostViewPage.tsx b/packages/web_ui/src/components/HostViewPage.tsx index c55eddd8..4d230ade 100644 --- a/packages/web_ui/src/components/HostViewPage.tsx +++ b/packages/web_ui/src/components/HostViewPage.tsx @@ -52,6 +52,21 @@ function HostControlButton({ hostId, canRestart, restartRequired }: HostControlB }); } + if (account.hasPermission("core.host.restart") && !restartRequired) { + actions.push({ + key: "mark-restart-required", + label: "Mark Restart Required", + onClick: () => { + control.sendTo( + { hostId }, + new lib.HostMarkRestartRequiredRequest() + ).then(() => { + notify("Host marked as requiring a restart", "success"); + }).catch(notifyErrorHandler("Error marking host")); + }, + }); + } + if (account.hasPermission("core.host.stop")) { actions.push({ key: "stop", diff --git a/test/messages/controller.test.js b/test/messages/controller.test.js index b7bd90cc..5a3a3ab3 100644 --- a/test/messages/controller.test.js +++ b/test/messages/controller.test.js @@ -29,6 +29,15 @@ describe("messages/controller", function() { controlConnection = new ControlConnection({ version: "2.0.0" }, connection, controller, user, 1); }); + describe("ControllerMarkRestartRequiredRequest", function() { + it("runs", async function() { + await controlConnection.handleControllerMarkRestartRequiredRequest( + new lib.ControllerMarkRestartRequiredRequest() + ); + assert.equal(controller.config.restartRequired, true); + }); + }); + describe("ControllerUpdateRequest", function() { it("runs", async function() { controller.config.set("controller.allow_remote_updates", true); diff --git a/test/messages/host.test.js b/test/messages/host.test.js index 25310748..1b31129a 100644 --- a/test/messages/host.test.js +++ b/test/messages/host.test.js @@ -32,6 +32,13 @@ describe("messages/host", function() { }); }); + describe("HostMarkRestartRequiredRequest", function() { + it("runs", async function() { + await host.handleHostMarkRestartRequiredRequest(new lib.HostMarkRestartRequiredRequest()); + assert.equal(host.config.restartRequired, true); + }); + }); + describe("HostConfigGetRequest", function() { it("runs", async function() { const config = await host.handleHostConfigGetRequest(new lib.HostConfigGetRequest());