Files
sso-manager-node/nodejs/models/plugin_instance.js
T
wmantly cec0d92c25 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>
2026-08-01 20:33:57 -04:00

82 lines
3.8 KiB
JavaScript

'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 };