mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Merge pull request #998 from bbassie/feature/update-all
Add update all for the controller, hosts and plugins
This commit is contained in:
12 files changed
+164
-7
No files matched your search
@@ -247,6 +247,13 @@ This command lists all plugins currently known to the remote. Newly installed pl
|
||||
|
||||
If remote plugin updates are enabled on the remote, this will execute `npm update --save <name>`. Changes will not be applied until the remote is restarted. If the `--restart` flag is provided, the remote will automatically restart once the update is complete.
|
||||
|
||||
### Update All
|
||||
|
||||
ctl> controller update --all [--restart]
|
||||
ctl> host update <host> --all [--restart]
|
||||
|
||||
Updates the Clusterio package and all plugins installed from npm in a single `npm update --save` command. Updating everything at once avoids ending up with multiple versions of `@clusterio/lib` in the installation. Requires both remote updates and remote plugin updates to be enabled on the remote.
|
||||
|
||||
### Install
|
||||
|
||||
ctl> controller plugin install <name> [--restart]
|
||||
|
||||
@@ -128,6 +128,7 @@ export default class ControlConnection extends BaseConnection {
|
||||
this.handle(lib.PluginListRequest, this.handlePluginListRequest.bind(this));
|
||||
this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this));
|
||||
this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this));
|
||||
this.handle(lib.UpdateAllRequest, this.handleUpdateAllRequest.bind(this));
|
||||
this.handle(lib.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this));
|
||||
}
|
||||
|
||||
@@ -1307,6 +1308,16 @@ export default class ControlConnection extends BaseConnection {
|
||||
return await lib.handlePluginInstall(request.pluginPackage);
|
||||
}
|
||||
|
||||
async handleUpdateAllRequest(request: lib.UpdateAllRequest) {
|
||||
if (!this._controller.config.get("controller.allow_remote_updates")) {
|
||||
throw new lib.RequestError("Remote updates are disabled on this machine");
|
||||
}
|
||||
if (!this._controller.config.get("controller.allow_plugin_updates")) {
|
||||
throw new lib.RequestError("Plugin updates are disabled on this machine");
|
||||
}
|
||||
return await lib.handleUpdateAll("@clusterio/controller", this._controller.pluginInfos);
|
||||
}
|
||||
|
||||
async handlePluginListRequest(request: lib.PluginListRequest) {
|
||||
return this._controller.pluginInfos.map(pluginInfo => lib.PluginDetails.fromNodeEnvInfo(
|
||||
pluginInfo,
|
||||
|
||||
@@ -28,9 +28,14 @@ controllerCommands.add(new lib.Command({
|
||||
controllerCommands.add(new lib.Command({
|
||||
definition: ["update", "Update the controller", (yargs) => {
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
yargs.option("all", { alias: "a", type: "boolean", description: "Also update all plugins" });
|
||||
}],
|
||||
handler: async function(args: { restart: boolean }, control: Control) {
|
||||
await control.send(new lib.ControllerUpdateRequest());
|
||||
handler: async function(args: { restart: boolean, all: boolean }, control: Control) {
|
||||
if (args.all) {
|
||||
await control.send(new lib.UpdateAllRequest());
|
||||
} else {
|
||||
await control.send(new lib.ControllerUpdateRequest());
|
||||
}
|
||||
if (args.restart) {
|
||||
await control.send(new lib.ControllerRestartRequest());
|
||||
} else {
|
||||
|
||||
@@ -101,10 +101,15 @@ hostCommands.add(new lib.Command({
|
||||
definition: ["update <host>", "Update the host", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host to update", type: "string" });
|
||||
yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" });
|
||||
yargs.option("all", { alias: "a", type: "boolean", description: "Also update all plugins" });
|
||||
}],
|
||||
handler: async function(args: { host: string, restart: boolean }, control: Control) {
|
||||
handler: async function(args: { host: string, restart: boolean, all: boolean }, control: Control) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
await control.sendTo({ hostId }, new lib.HostUpdateRequest());
|
||||
if (args.all) {
|
||||
await control.sendTo({ hostId }, new lib.UpdateAllRequest());
|
||||
} else {
|
||||
await control.sendTo({ hostId }, new lib.HostUpdateRequest());
|
||||
}
|
||||
if (args.restart) {
|
||||
await control.sendTo({ hostId }, new lib.HostRestartRequest());
|
||||
} else {
|
||||
|
||||
@@ -404,6 +404,7 @@ export default class Host extends lib.Link {
|
||||
this.handle(lib.PluginListRequest, this.handlePluginListRequest.bind(this));
|
||||
this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this));
|
||||
this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this));
|
||||
this.handle(lib.UpdateAllRequest, this.handleUpdateAllRequest.bind(this));
|
||||
|
||||
this.snoopEvent(lib.InstanceAdminlistUpdateEvent, this.handleAdminlistUpdateEvent.bind(this));
|
||||
this.snoopEvent(lib.InstanceBanlistUpdateEvent, this.handleBanlistUpdateEvent.bind(this));
|
||||
@@ -1151,6 +1152,16 @@ export default class Host extends lib.Link {
|
||||
return await lib.handlePluginInstall(request.pluginPackage);
|
||||
}
|
||||
|
||||
async handleUpdateAllRequest(request: lib.UpdateAllRequest) {
|
||||
if (!this.config.get("host.allow_remote_updates")) {
|
||||
throw new lib.RequestError("Remote updates are disabled on this machine");
|
||||
}
|
||||
if (!this.config.get("host.allow_plugin_updates")) {
|
||||
throw new lib.RequestError("Plugin updates are disabled on this machine");
|
||||
}
|
||||
return await lib.handleUpdateAll("@clusterio/host", this.pluginInfos);
|
||||
}
|
||||
|
||||
async handlePluginListRequest(request: lib.PluginListRequest) {
|
||||
return this.pluginInfos.map(pluginInfo => lib.PluginDetails.fromNodeEnvInfo(
|
||||
pluginInfo,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
import { jsonArray } from "./composites.js";
|
||||
import { PluginNodeEnvInfo } from "../plugin.js";
|
||||
import { Address, MessageRequest } from "./messages_core.js";
|
||||
import type { IUser } from "./UserDetails.js";
|
||||
|
||||
/* This is similar to other plugin definitions but with lots removed */
|
||||
export class PluginDetails {
|
||||
@@ -91,3 +93,14 @@ export class PluginInstallRequest {
|
||||
return this.pluginPackage;
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateAllRequest {
|
||||
declare ["constructor"]: typeof UpdateAllRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = ["controller", "host"] as const;
|
||||
static permission(user: IUser, message: MessageRequest) {
|
||||
user.checkPermission(message.dst.type === Address.host ? "core.host.update" : "core.controller.update");
|
||||
user.checkPermission("core.plugin.update");
|
||||
}
|
||||
}
|
||||
@@ -140,4 +140,5 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
||||
plugin.PluginListRequest,
|
||||
plugin.PluginUpdateRequest,
|
||||
plugin.PluginInstallRequest,
|
||||
plugin.UpdateAllRequest,
|
||||
];
|
||||
@@ -19,8 +19,8 @@ async function logExec(cmd: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePackage(name: string) {
|
||||
return logExec(`npm update --save ${name}`);
|
||||
export async function updatePackage(...names: string[]) {
|
||||
return logExec(`npm update --save ${names.join(" ")}`);
|
||||
}
|
||||
|
||||
export async function installPackage(name: string) {
|
||||
@@ -35,6 +35,13 @@ export async function handlePluginUpdate(pluginName: string, pluginInfos: Plugin
|
||||
return await updatePackage(pluginName);
|
||||
}
|
||||
|
||||
export async function handleUpdateAll(corePackage: string, pluginInfos: PluginNodeEnvInfo[]) {
|
||||
const pluginPackages = pluginInfos
|
||||
.map(plugin => plugin.npmPackage)
|
||||
.filter((name): name is string => name !== undefined);
|
||||
return await updatePackage(corePackage, ...pluginPackages);
|
||||
}
|
||||
|
||||
export async function handlePluginInstall(pluginName: string) {
|
||||
if (pluginName.length > 214 || /[^a-zA-Z0-9\-_.+@\/]/.test(pluginName)) {
|
||||
// https://docs.npmjs.com/cli/v11/configuring-npm/package-json#name
|
||||
|
||||
@@ -27,6 +27,17 @@ function NpmRemoteUpdate({ setApplyAction, form, target }: NpmActionProps) {
|
||||
</>;
|
||||
}
|
||||
|
||||
function NpmUpdateAll({ setApplyAction, form, target }: NpmActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
|
||||
setApplyAction(async () => {
|
||||
await control.sendTo(target, new lib.UpdateAllRequest());
|
||||
});
|
||||
|
||||
return <>
|
||||
</>;
|
||||
}
|
||||
|
||||
function NpmPluginUpdate({ setApplyAction, form, target }: NpmActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
const [plugins, setPlugins] = useState<lib.PluginDetails[]>([]);
|
||||
@@ -128,6 +139,8 @@ export function NpmButton(props: {
|
||||
}
|
||||
}, [hostId]);
|
||||
|
||||
const coreUpdatePermission = `core.${props.target === "controller" ? "controller" : "host"}.update` as const;
|
||||
|
||||
function onValuesChange({ action } : { action?: string }) {
|
||||
if (action) {
|
||||
setFormAction(action);
|
||||
@@ -175,10 +188,17 @@ export function NpmButton(props: {
|
||||
<Form form={form} onValuesChange={onValuesChange} clearOnDestroy>
|
||||
<Form.Item label="Action" name="action">
|
||||
<Radio.Group value={formAction}>
|
||||
{account.hasPermission(`core.${props.target === "controller" ? "controller" : "host"}.update`)
|
||||
{account.hasPermission(coreUpdatePermission)
|
||||
? <Radio.Button value="remote_update" disabled={!allows.remoteUpdates}>
|
||||
Update Clusterio
|
||||
</Radio.Button>: undefined}
|
||||
{account.hasPermission(coreUpdatePermission) && account.hasPermission("core.plugin.update")
|
||||
? <Radio.Button
|
||||
value="update_all"
|
||||
disabled={!allows.remoteUpdates || !allows.pluginUpdates}
|
||||
>
|
||||
Update All
|
||||
</Radio.Button> : undefined}
|
||||
{account.hasPermission("core.plugin.update")
|
||||
? <Radio.Button value="plugin_update" disabled={!allows.pluginUpdates}>
|
||||
Update Plugin
|
||||
@@ -198,6 +218,8 @@ export function NpmButton(props: {
|
||||
}
|
||||
{formAction === "remote_update"
|
||||
? <NpmRemoteUpdate {...{setApplyAction, form, target: props.target}}/> : undefined}
|
||||
{formAction === "update_all"
|
||||
? <NpmUpdateAll {...{setApplyAction, form, target: props.target}}/> : undefined}
|
||||
{formAction === "plugin_update"
|
||||
? <NpmPluginUpdate {...{setApplyAction, form, target: props.target}}/> : undefined}
|
||||
{formAction === "plugin_install"
|
||||
|
||||
@@ -521,6 +521,9 @@ describe("Integration of Clusterio", function() {
|
||||
it("runs", async function() {
|
||||
await execCtl("controller update");
|
||||
});
|
||||
it("accepts --all", async function() {
|
||||
await execCtl("controller update --all");
|
||||
});
|
||||
it("accepts --restart", async function() {
|
||||
// We cannot restart the controller, so we check for controller error instead
|
||||
await assert.rejects(
|
||||
@@ -603,6 +606,9 @@ describe("Integration of Clusterio", function() {
|
||||
it("runs", async function() {
|
||||
await execCtl("host update 4");
|
||||
});
|
||||
it("accepts --all", async function() {
|
||||
await execCtl("host update 4 --all");
|
||||
});
|
||||
it("accepts --restart", async function() {
|
||||
// We cannot restart the host, so we check for host error instead
|
||||
await assert.rejects(
|
||||
|
||||
@@ -28,6 +28,20 @@ describe("rce_ops", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleUpdateAll", function() {
|
||||
it("updates the core package and all npm plugins in one command", async function() {
|
||||
const _audit = lib.logger.audit;
|
||||
const messages = [];
|
||||
lib.logger.audit = (msg) => { messages.push(msg); };
|
||||
try {
|
||||
await lib.handleUpdateAll("core", [{ npmPackage: "foo" }, { name: "local" }, { npmPackage: "bar" }]);
|
||||
} finally {
|
||||
lib.logger.audit = _audit;
|
||||
}
|
||||
assert.deepEqual(messages, ["RCE | npm update --save core foo bar"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handlePluginInstall", function() {
|
||||
const _fetch = global.fetch;
|
||||
before(function() {
|
||||
|
||||
@@ -133,6 +133,61 @@ describe("messages/plugin", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("UpdateAllRequest", function() {
|
||||
function checkedPermissions(dst) {
|
||||
const permissions = [];
|
||||
const user = { checkPermission(permission) { permissions.push(permission); } };
|
||||
lib.UpdateAllRequest.permission(user, { dst: lib.Address.fromShorthand(dst) });
|
||||
return permissions;
|
||||
}
|
||||
it("requires controller and plugin update permissions on the controller", function() {
|
||||
assert.deepEqual(checkedPermissions("controller"), ["core.controller.update", "core.plugin.update"]);
|
||||
});
|
||||
it("requires host and plugin update permissions on a host", function() {
|
||||
assert.deepEqual(checkedPermissions({ hostId: 1 }), ["core.host.update", "core.plugin.update"]);
|
||||
});
|
||||
it("runs on the controller", async function() {
|
||||
controller.pluginInfos = [{ npmPackage: "foo" }];
|
||||
controller.config.set("controller.allow_remote_updates", true);
|
||||
controller.config.set("controller.allow_plugin_updates", true);
|
||||
await controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest());
|
||||
});
|
||||
it("rejects if remote updates are disabled on the controller", async function() {
|
||||
controller.config.set("controller.allow_remote_updates", false);
|
||||
await assert.rejects(
|
||||
controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()),
|
||||
/Remote updates are disabled on this machine/
|
||||
);
|
||||
});
|
||||
it("rejects if plugin updates are disabled on the controller", async function() {
|
||||
controller.config.set("controller.allow_plugin_updates", false);
|
||||
await assert.rejects(
|
||||
controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()),
|
||||
/Plugin updates are disabled on this machine/
|
||||
);
|
||||
});
|
||||
it("runs on a host", async function() {
|
||||
host.pluginInfos = [{ npmPackage: "foo" }];
|
||||
host.config.set("host.allow_remote_updates", true);
|
||||
host.config.set("host.allow_plugin_updates", true);
|
||||
await host.handleUpdateAllRequest(new lib.UpdateAllRequest());
|
||||
});
|
||||
it("rejects if remote updates are disabled on the host", async function() {
|
||||
host.config.set("host.allow_remote_updates", false);
|
||||
await assert.rejects(
|
||||
host.handleUpdateAllRequest(new lib.UpdateAllRequest()),
|
||||
/Remote updates are disabled on this machine/
|
||||
);
|
||||
});
|
||||
it("rejects if plugin updates are disabled on the host", async function() {
|
||||
host.config.set("host.allow_plugin_updates", false);
|
||||
await assert.rejects(
|
||||
host.handleUpdateAllRequest(new lib.UpdateAllRequest()),
|
||||
/Plugin updates are disabled on this machine/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PluginInstallRequest", function() {
|
||||
const _fetch = global.fetch;
|
||||
before(function() {
|
||||
|
||||
Reference in new issue
Block a user