release: v1.32.0 - Subtype Drivers Engine, Explicit Secret Inheritance & App Tokens consolidation
This commit is contained in:
@@ -19,9 +19,42 @@ function isDescendant(candidateId, rootId, edges) {
|
||||
}
|
||||
|
||||
class DiscoveryReconciler {
|
||||
static async reconcile(sourceName, payload) {
|
||||
static async reconcile(sourceName, payload, options = {}) {
|
||||
const { resources = [], edges = [] } = payload;
|
||||
let newDevices = 0;
|
||||
const location = options.location || options.site || null;
|
||||
const autoPromote = !!options.autoPromote;
|
||||
|
||||
let targetSite = null;
|
||||
if (location && String(location).trim()) {
|
||||
const sites = await Resource.list({ where: { kind: 'site' } });
|
||||
const locStr = String(location).trim().toLowerCase();
|
||||
targetSite = sites.find(s => s.name.toLowerCase() === locStr || s.slug.toLowerCase() === locStr);
|
||||
if (!targetSite) {
|
||||
const locSlug = `site-${locStr.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`;
|
||||
targetSite = await Resource.create({
|
||||
id: crypto.randomUUID(),
|
||||
kind: 'site',
|
||||
name: String(location).trim(),
|
||||
slug: locSlug,
|
||||
created_on: Math.floor(Date.now() / 1000)
|
||||
}).catch(() => null);
|
||||
}
|
||||
}
|
||||
if (!targetSite) {
|
||||
const sites = await Resource.list({ where: { kind: 'site' } });
|
||||
if (sites && sites.length > 0) {
|
||||
targetSite = sites[0];
|
||||
} else {
|
||||
targetSite = await Resource.create({
|
||||
id: crypto.randomUUID(),
|
||||
kind: 'site',
|
||||
name: 'Default Site',
|
||||
slug: 'site-default',
|
||||
created_on: Math.floor(Date.now() / 1000)
|
||||
}).catch(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeMac = (m) => (m || '').toLowerCase().replace(/[^a-f0-9]/g, '');
|
||||
const normalizeHost = (h) => (h || '').toLowerCase().split('.')[0].trim();
|
||||
@@ -35,6 +68,7 @@ class DiscoveryReconciler {
|
||||
|
||||
for (const res of resources) {
|
||||
if (!res.metadata) res.metadata = {};
|
||||
if (autoPromote) res.metadata.managed = true;
|
||||
res._originalSlug = res.slug; // Keep track for edge mapping
|
||||
|
||||
let existing = null;
|
||||
@@ -255,6 +289,45 @@ class DiscoveryReconciler {
|
||||
}
|
||||
}
|
||||
|
||||
if (targetSite) {
|
||||
const childSlugs = new Set(edges.map(e => e.childSlug));
|
||||
for (const res of resources) {
|
||||
if (res._actualId && res._actualId !== targetSite.id && !childSlugs.has(res._originalSlug || res.slug)) {
|
||||
const edgeExists = existingEdges.find(e => e.childId === res._actualId);
|
||||
if (!edgeExists) {
|
||||
const created = await ResourceEdge.create({
|
||||
id: crypto.randomUUID(),
|
||||
parentId: targetSite.id,
|
||||
childId: res._actualId,
|
||||
relation: 'hosts'
|
||||
}).catch(() => null);
|
||||
if (created) existingEdges.push(created);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (autoPromote) {
|
||||
const { Group } = require('../models/group_ldap');
|
||||
for (const res of resources) {
|
||||
if (!res._actualId) continue;
|
||||
const accessGroup = `${res.slug}_access`;
|
||||
const adminGroup = `${res.slug}_admin`;
|
||||
try {
|
||||
await Group.get(accessGroup).catch(async (e) => {
|
||||
if (e.status === 404) await Group.add({ name: accessGroup, description: `Access to ${res.name}`, owner: 'cn=admin' });
|
||||
});
|
||||
await Group.get(adminGroup).catch(async (e) => {
|
||||
if (e.status === 404) await Group.add({ name: adminGroup, description: `Admin access to ${res.name}`, owner: 'cn=admin' });
|
||||
});
|
||||
await ResourceGroup.create({ id: crypto.randomUUID(), resourceId: res._actualId, groupCn: accessGroup, accessLevel: 'user' }).catch(() => {});
|
||||
await ResourceGroup.create({ id: crypto.randomUUID(), resourceId: res._actualId, groupCn: adminGroup, accessLevel: 'admin' }).catch(() => {});
|
||||
} catch (err) {
|
||||
console.error(`[DiscoveryReconciler] autoPromote failed for ${res.slug}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newDevices > 0) {
|
||||
console.log(`[DiscoveryReconciler] Source ${sourceName} discovered ${newDevices} new devices.`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
const BaseDriver = require('../drivers/base_driver');
|
||||
const ThetaAgentDriver = require('../drivers/theta_agent_driver');
|
||||
const ProxmoxDriver = require('../drivers/proxmox_driver');
|
||||
const DockerSocketDriver = require('../drivers/docker_socket_driver');
|
||||
const DbDriver = require('../drivers/db_driver');
|
||||
const NetworkDriver = require('../drivers/network_driver');
|
||||
const K8sDriver = require('../drivers/k8s_driver');
|
||||
const AgentManager = require('../utils/agent_manager');
|
||||
|
||||
/**
|
||||
* Registry & Resolution Engine for Subtype Management and Metrics Drivers.
|
||||
*/
|
||||
class DriverRegistry {
|
||||
constructor() {
|
||||
this.drivers = [];
|
||||
this.defaultDriver = new BaseDriver('unmanaged');
|
||||
this.initDefaultDrivers();
|
||||
}
|
||||
|
||||
initDefaultDrivers() {
|
||||
this.thetaAgentDriver = new ThetaAgentDriver();
|
||||
this.proxmoxDriver = new ProxmoxDriver();
|
||||
this.dockerSocketDriver = new DockerSocketDriver();
|
||||
this.dbDriver = new DbDriver();
|
||||
this.networkDriver = new NetworkDriver();
|
||||
this.k8sDriver = new K8sDriver();
|
||||
|
||||
// Register drivers in priority order
|
||||
this.register(this.thetaAgentDriver);
|
||||
this.register(this.proxmoxDriver);
|
||||
this.register(this.dockerSocketDriver);
|
||||
this.register(this.dbDriver);
|
||||
this.register(this.networkDriver);
|
||||
this.register(this.k8sDriver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new subtype driver.
|
||||
* @param {BaseDriver} driver
|
||||
*/
|
||||
register(driver) {
|
||||
if (driver && typeof driver.getMetrics === 'function') {
|
||||
this.drivers.push(driver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the best driver for a resource using the 4-tier resolution engine:
|
||||
* 1. Direct theta-agent (if agent connected)
|
||||
* 2. Subtype-specific driver (Proxmox, Docker, DB, Network, K8s)
|
||||
* 3. Parent Provider Fallback (e.g. Proxmox hypervisor host for un-agentized LXC/KVM guest)
|
||||
* 4. Unmanaged fallback
|
||||
* @param {Object} resource
|
||||
* @returns {BaseDriver}
|
||||
*/
|
||||
async resolveDriver(resource) {
|
||||
if (!resource) return this.defaultDriver;
|
||||
|
||||
// 1. Direct Theta Agent Check
|
||||
const agent = await AgentManager.getAgentForResource(resource.id).catch(() => null);
|
||||
if (agent && agent.isOnline) {
|
||||
return this.thetaAgentDriver;
|
||||
}
|
||||
|
||||
// 2. Specialized Subtype Driver Check
|
||||
for (const driver of this.drivers) {
|
||||
if (driver !== this.thetaAgentDriver && driver.supports(resource)) {
|
||||
return driver;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to Theta Agent if bound (even if offline, so offline status is reported)
|
||||
if (agent) {
|
||||
return this.thetaAgentDriver;
|
||||
}
|
||||
|
||||
// 4. Fallback to Proxmox driver if it's an LXC/KVM guest
|
||||
const subType = ((resource.metadata && resource.metadata.subType) || '').toLowerCase();
|
||||
if (['lxc', 'kvm'].includes(subType)) {
|
||||
return this.proxmoxDriver;
|
||||
}
|
||||
|
||||
return this.defaultDriver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operational telemetry for a resource.
|
||||
*/
|
||||
async getMetrics(resource, options = {}) {
|
||||
const driver = await this.resolveDriver(resource);
|
||||
return await driver.getMetrics(resource, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a management action on a resource.
|
||||
*/
|
||||
async execAction(resource, action, params = {}) {
|
||||
const driver = await this.resolveDriver(resource);
|
||||
return await driver.execAction(resource, action, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recent logs for a resource.
|
||||
*/
|
||||
async getLogs(resource, lines = 100) {
|
||||
const driver = await this.resolveDriver(resource);
|
||||
return await driver.getLogs(resource, lines);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const registry = new DriverRegistry();
|
||||
module.exports = registry;
|
||||
@@ -82,7 +82,7 @@ async function runPluginJob(instanceId) {
|
||||
};
|
||||
const payload = await runFn(cfg);
|
||||
if (instance.category === 'discovery') {
|
||||
await DiscoveryReconciler.reconcile(instance.slug, payload);
|
||||
await DiscoveryReconciler.reconcile(instance.slug, payload, cfg);
|
||||
}
|
||||
await instance.update({ lastStatus: STATUS.OK, lastError: null, lastLog: logs.join('\n') });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user