diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 673a9277..5e827a89 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -73,6 +73,7 @@ export default class ControlConnection extends BaseConnection { this.handle(lib.ControllerRestartRequest, this.handleControllerRestartRequest.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)); this.handle(lib.ControllerConfigSetFieldRequest, this.handleControllerConfigSetFieldRequest.bind(this)); this.handle(lib.ControllerConfigSetPropRequest, this.handleControllerConfigSetPropRequest.bind(this)); this.handle(lib.HostRevokeTokensRequest, this.handleHostRevokeTokensRequest.bind(this)); @@ -84,6 +85,7 @@ export default class ControlConnection extends BaseConnection { this.handle(lib.InstanceCreateRequest, this.handleInstanceCreateRequest.bind(this)); this.handle(lib.InstanceDeleteRequest, this.handleInstanceDeleteRequest.bind(this)); this.handle(lib.InstanceConfigGetRequest, this.handleInstanceConfigGetRequest.bind(this)); + this.handle(lib.InstanceConfigSetRequest, this.handleInstanceConfigSetRequest.bind(this)); this.handle(lib.InstanceConfigSetFieldRequest, this.handleInstanceConfigSetFieldRequest.bind(this)); this.handle(lib.InstanceConfigSetPropRequest, this.handleInstanceConfigSetPropRequest.bind(this)); this.handle(lib.InstanceAssignRequest, this.handleInstanceAssignRequest.bind(this)); @@ -196,6 +198,26 @@ export default class ControlConnection extends BaseConnection { return this._controller.config.toRemote("control"); } + async handleControllerConfigSetRequest(request: lib.ControllerConfigSetRequest) { + try { + for (const [fieldName, fieldValue] of Object.entries(request.fields)) { + if (typeof fieldValue === "object") { + for (const [propName, propValue] of Object.entries(fieldValue)) { + this._controller.config.stageProp( + fieldName as keyof lib.ControllerConfigFields, propName, propValue, "control" + ); + } + } else { + this._controller.config.stage(fieldName as keyof lib.ControllerConfigFields, fieldValue, "control"); + } + } + + this._controller.config.commitStaging(); + } finally { + this._controller.config.revertStaging(); + } + } + async handleControllerConfigSetFieldRequest(request: lib.ControllerConfigSetFieldRequest) { this._controller.config.set(request.field as keyof lib.ControllerConfigFields, request.value, "control"); } @@ -281,17 +303,41 @@ export default class ControlConnection extends BaseConnection { return instance.config.toRemote("control"); } - async handleInstanceConfigSetFieldRequest(request: lib.InstanceConfigSetFieldRequest) { - let instance = this._controller.instances.getForRequest(request.instanceId); - if (request.field === "instance.assigned_host") { + private static validateInstanceConfigSetField(fieldName: string) { + if (fieldName === "instance.assigned_host") { throw new lib.RequestError("instance.assigned_host must be set through the assign-host interface"); } - if (request.field === "instance.id") { + if (fieldName === "instance.id") { // XXX is this worth implementing? It's race condition galore. throw new lib.RequestError("Setting instance.id is not supported"); } + } + async handleInstanceConfigSetRequest(request: lib.InstanceConfigSetRequest) { + try { + for (const [fieldName, fieldValue] of Object.entries(request.fields)) { + ControlConnection.validateInstanceConfigSetField(fieldName); + if (typeof fieldValue === "object") { + for (const [propName, propValue] of Object.entries(fieldValue)) { + this._controller.config.stageProp( + fieldName as keyof lib.ControllerConfigFields, propName, propValue, "control" + ); + } + } else { + this._controller.config.stage(fieldName as keyof lib.ControllerConfigFields, fieldValue, "control"); + } + } + + this._controller.config.commitStaging(); + } finally { + this._controller.config.revertStaging(); + } + } + + async handleInstanceConfigSetFieldRequest(request: lib.InstanceConfigSetFieldRequest) { + const instance = this._controller.instances.getForRequest(request.instanceId); + ControlConnection.validateInstanceConfigSetField(request.field); instance.config.set(request.field as keyof lib.InstanceConfigFields, request.value, "control"); await this._controller.instanceConfigUpdated(instance); } @@ -299,6 +345,7 @@ export default class ControlConnection extends BaseConnection { async handleInstanceConfigSetPropRequest(request: lib.InstanceConfigSetPropRequest) { let instance = this._controller.instances.getForRequest(request.instanceId); let { field, prop, value } = request; + ControlConnection.validateInstanceConfigSetField(field); instance.config.setProp(field as keyof lib.InstanceConfigFields, prop, value, "control"); await this._controller.instanceConfigUpdated(instance); } diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index e59d6593..f33bb604 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.HostRestartRequest, this.handleHostRestartRequest.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)); this.handle(lib.HostConfigSetFieldRequest, this.handleHostConfigSetFieldRequest.bind(this)); this.handle(lib.HostConfigSetPropRequest, this.handleHostConfigSetPropRequest.bind(this)); this.handle(lib.HostMetricsRequest, this.handleHostMetricsRequest.bind(this)); @@ -503,18 +504,41 @@ export default class Host extends lib.Link { return this.config.toRemote("control"); } - async handleHostConfigSetFieldRequest(request: lib.HostConfigSetFieldRequest) { - if (request.field === "host.id") { + private static validateHostConfigSetField(fieldName: string) { + if (fieldName === "host.id") { // Changing host.id at runtime is not worth the effort to // support and will break a lot of things if it is allowed. throw new lib.RequestError("Setting 'host.id' while host is running is not supported"); } + } + async handleHostConfigSetRequest(request: lib.HostConfigSetRequest) { + try { + for (const [fieldName, fieldValue] of Object.entries(request.fields)) { + Host.validateHostConfigSetField(fieldName); + if (typeof fieldValue === "object") { + for (const [propName, propValue] of Object.entries(fieldValue)) { + this.config.stageProp(fieldName as keyof lib.HostConfigFields, propName, propValue, "control"); + } + } else { + this.config.stage(fieldName as keyof lib.HostConfigFields, fieldValue, "control"); + } + } + + this.config.commitStaging(); + } finally { + this.config.revertStaging(); + } + } + + async handleHostConfigSetFieldRequest(request: lib.HostConfigSetFieldRequest) { + Host.validateHostConfigSetField(request.field); this.config.set(request.field as keyof lib.HostConfigFields, request.value, "control"); } async handleHostConfigSetPropRequest(request: lib.HostConfigSetPropRequest) { let { field, prop, value } = request; + Host.validateHostConfigSetField(field); this.config.setProp(field as keyof lib.HostConfigFields, prop, value, "control"); } diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index 2c74b4c2..d4206a39 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -37,6 +37,28 @@ export class ControllerConfigGetRequest { static Response = plainJson(ControllerConfig.jsonSchema); } +export class ControllerConfigSetRequest { + declare ["constructor"]: typeof ControllerConfigSetRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.controller.update_config" as const; + + constructor( + public fields: Record>, + ) { } + + static jsonSchema = Type.Object({ + "fields": Type.Record(Type.String(), Type.Union([ + Type.String(), Type.Record(Type.String(), Type.String()), + ])), + }); + + static fromJSON(json: Static) { + return new this(json.fields); + } +} + export class ControllerConfigSetFieldRequest { declare ["constructor"]: typeof ControllerConfigSetFieldRequest; static type = "request" as const; diff --git a/packages/lib/src/data/messages_host.ts b/packages/lib/src/data/messages_host.ts index f1499992..67cf060b 100644 --- a/packages/lib/src/data/messages_host.ts +++ b/packages/lib/src/data/messages_host.ts @@ -37,6 +37,28 @@ export class HostConfigGetRequest { static Response = plainJson(HostConfig.jsonSchema); } +export class HostConfigSetRequest { + declare ["constructor"]: typeof HostConfigSetRequest; + static type = "request" as const; + static src = ["control", "controller"] as const; + static dst = "host" as const; + static permission = "core.host.update_config" as const; + + constructor( + public fields: Record>, + ) { } + + static jsonSchema = Type.Object({ + "fields": Type.Record(Type.String(), Type.Union([ + Type.String(), Type.Record(Type.String(), Type.String()), + ])), + }); + + static fromJSON(json: Static) { + return new this(json.fields); + } +} + export class HostConfigSetFieldRequest { declare ["constructor"]: typeof HostConfigSetFieldRequest; static type = "request" as const; diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index d138f3ea..7c9d7693 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -102,6 +102,30 @@ export class InstanceConfigGetRequest { } } +export class InstanceConfigSetRequest { + declare ["constructor"]: typeof InstanceConfigSetRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.instance.update_config" as const; + + constructor( + public instanceId: number, + public fields: Record>, + ) { } + + static jsonSchema = Type.Object({ + "instanceId": Type.Integer(), + "fields": Type.Record(Type.String(), Type.Union([ + Type.String(), Type.Record(Type.String(), Type.String()), + ])), + }); + + static fromJSON(json: Static) { + return new this(json.instanceId, json.fields); + } +} + export class InstanceConfigSetFieldRequest { declare ["constructor"]: typeof InstanceConfigSetFieldRequest; static type = "request" as const; diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index 299cf3ae..419385ad 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -23,6 +23,7 @@ export const dataClasses: (RequestClass | EventClass) controller.ControllerRestartRequest, controller.ControllerUpdateRequest, controller.ControllerConfigGetRequest, + controller.ControllerConfigSetRequest, controller.ControllerConfigSetFieldRequest, controller.ControllerConfigSetPropRequest, controller.HostGenerateTokenRequest, @@ -43,6 +44,7 @@ export const dataClasses: (RequestClass | EventClass) host.HostRestartRequest, host.HostUpdateRequest, host.HostConfigGetRequest, + host.HostConfigSetRequest, host.HostConfigSetFieldRequest, host.HostConfigSetPropRequest, host.HostListRequest, @@ -59,6 +61,7 @@ export const dataClasses: (RequestClass | EventClass) instance.InstanceDetailsUpdatesEvent, instance.InstanceCreateRequest, instance.InstanceConfigGetRequest, + instance.InstanceConfigSetRequest, instance.InstanceConfigSetFieldRequest, instance.InstanceConfigSetPropRequest, instance.InstanceAssignRequest,