feat: real plugin system with loadable instances + OpenBao secrets (v1.17.0)
Generalize the half-built discovery plugins into a real plugin system: plugin TYPES (the plugins/<category>/<type>.js modules with manifests) and loadable, configurable, multi-copy plugin INSTANCES (PluginInstance ORM model) managed from a dedicated /plugins page and /api/plugins API, with per-instance secrets in OpenBao at secret/plugins/<id>/conf. - plugin_registry.js: getTypes/getModule/splitConfig/mask + required-field helpers - PluginInstance model (Sequelize): id/pluginType/category/name/slug(unique)/ enabled/cron/config(json, non-secret)/lastRun*; registered in models/index.js - plugin_secrets.js: read/write/remove/mergeForRun over @simpleworkjs/bao-conf - scheduler.js: schedules from the DB registry; per-instance stable BullMQ JobScheduler ids (plugin:<id>) for load/unload; legacy migration from conf.discovery.plugins on first boot (idempotent, empty-table-guarded) - api_plugins.js (replaces routes/plugins.js): types/list/get/create/update/ secrets/test/load/unload/run/delete/runs; admin-gated; secrets always masked - /plugins page (plugins.ejs) + nav; Agents & Scheduler tab removed from /directory; /docs/agents aliased to /docs/plugins - proxmox/unifi/nmap gained manifests (configSchema/validate/run alias) - tests/plugins.test.js: registry unit + plugin_secrets (mocked bao-conf) + PluginInstance model round-trip/unique-slug - docs (plugins.md, vault.md, _config.yml, API.md) + 1.16.1 -> 1.17.0 Requires theta-suite >= v1.30.1 for the sso-broker secret/plugins/* grant; fails-soft with a clear error if absent. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ const { init } = require('@simpleworkjs/orm');
|
||||
const { Resource, ResourceEdge, ResourceGroup } = require('./resource');
|
||||
const { AccessRequest } = require('./access_request');
|
||||
const { Webhook } = require('./webhook');
|
||||
const { PluginInstance } = require('./plugin_instance');
|
||||
async function initORM() {
|
||||
const ormConf = conf.orm || {
|
||||
dialect: 'sqlite',
|
||||
@@ -29,7 +30,7 @@ async function initORM() {
|
||||
await init({
|
||||
conf: { orm: ormConf },
|
||||
models: [
|
||||
Resource, ResourceEdge, ResourceGroup, AccessRequest, Webhook,
|
||||
Resource, ResourceEdge, ResourceGroup, AccessRequest, Webhook, PluginInstance,
|
||||
Token, AuthToken, InviteToken, ImpersonationToken, PasswordResetToken, OtpToken, ServiceToken
|
||||
]
|
||||
});
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
// PluginInstance — the registry of configured, loadable plugin copies.
|
||||
//
|
||||
// The SSO plugin system (see nodejs/services/plugin_registry.js) distinguishes
|
||||
// **plugin types** (the .js modules under nodejs/plugins/<category>/<type>.js)
|
||||
// from **plugin instances** — a configured, loadable/unloadable *copy* of a
|
||||
// type. You can have several instances of the same type (e.g. two Proxmox
|
||||
// endpoints with their own URLs + tokens), each on its own schedule.
|
||||
//
|
||||
// This table holds the *non-secret* per-instance state: which type it is, its
|
||||
// schedule (cron), whether it's loaded (enabled), and its non-secret config.
|
||||
// Per-instance **secrets** (the configSchema fields flagged `secret:true`,
|
||||
// e.g. a Proxmox `tokenSecret` or UniFi `password`) live in OpenBao at
|
||||
// `secret/plugins/<id>/conf` (see nodejs/utils/plugin_secrets.js) — never in
|
||||
// the DB. The DB row's `config` JSON column holds only non-secret field values.
|
||||
//
|
||||
// `slug` is the discovery source name passed to DiscoveryReconciler.reconcile,
|
||||
// so a discovery instance's resources are attributed to a stable, human-chosen
|
||||
// name rather than its uuid. Unique, so two instances can't shadow each other
|
||||
// in the resource graph's `discovery_sources`.
|
||||
//
|
||||
// Like Resource/AccessRequest, there is no ORM auto-timestamp hook: the route
|
||||
// handler stamps created_by/on + updated_by/on explicitly on every write (see
|
||||
// routes/api_plugins.js). `id` (uuid) is generated by the ORM on create.
|
||||
|
||||
const { Model } = require('@simpleworkjs/orm');
|
||||
|
||||
const STATUS = {
|
||||
OK: 'ok',
|
||||
ERROR: 'error',
|
||||
RUNNING: 'running',
|
||||
};
|
||||
|
||||
class PluginInstance extends Model {
|
||||
static fields = {
|
||||
id: { type: 'uuid', primaryKey: true },
|
||||
// A registered plugin type slug (matches a manifest `type`). Validated
|
||||
// against the registry before a row is created.
|
||||
pluginType: { type: 'string', isRequired: true, min: 1, max: 64 },
|
||||
// The plugin's category (e.g. 'discovery'). Copied from the manifest at
|
||||
// create time so the scheduler can dispatch without re-reading the registry
|
||||
// on every run (and so a later type removal still shows what the instance was).
|
||||
category: { type: 'string', isRequired: true, default: 'discovery', min: 1, max: 64 },
|
||||
// Human label for the instance.
|
||||
name: { type: 'string', isRequired: true, min: 1, max: 120 },
|
||||
// Stable handle: discovery source name + unique constraint. Lowercase
|
||||
// alnum + hyphen/underscore to stay safe as a resource-graph slug.
|
||||
slug: { type: 'string', isRequired: true, unique: true, min: 1, max: 64 },
|
||||
// Loaded into the scheduler? `false` = unloaded (no scheduled runs).
|
||||
enabled: { type: 'boolean', default: true },
|
||||
// Cron schedule (5-field). The scheduler turns this into a BullMQ
|
||||
// repeatable JobScheduler.
|
||||
cron: { type: 'string', isRequired: true, default: '0 * * * *' },
|
||||
// Non-secret configSchema field values. Secret fields are NOT here.
|
||||
config: { type: 'json', default: {} },
|
||||
// Last-run bookkeeping, updated by the scheduler worker.
|
||||
lastRunAt: { type: 'integer' },
|
||||
lastStatus: { type: 'string' },
|
||||
lastError: { type: 'text' },
|
||||
// Audit stamps (set by the route handler, not by an ORM hook).
|
||||
created_by: { type: 'string' },
|
||||
created_on: { type: 'integer' },
|
||||
updated_by: { type: 'string' },
|
||||
updated_on: { type: 'integer' },
|
||||
};
|
||||
|
||||
// All instances the scheduler should run: enabled only. Loaded fresh each
|
||||
// boot / load; not cached on the model (the scheduler is the source of truth
|
||||
// for what's actually scheduled).
|
||||
static async listEnabled() {
|
||||
return this.list({ where: { enabled: true } });
|
||||
}
|
||||
|
||||
// Look up by slug — used by tests + the reconciler when only a slug is known.
|
||||
static async getBySlug(slug) {
|
||||
const rows = await this.list({ where: { slug } });
|
||||
return rows[0] || null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PluginInstance, STATUS };
|
||||
Reference in New Issue
Block a user