cec0d92c25
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>
82 lines
3.8 KiB
JavaScript
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 }; |