timonel 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +432 -1
- package/SECURITY.md +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/policy/configurationLoader.d.ts +46 -0
- package/dist/lib/policy/configurationLoader.js +251 -0
- package/dist/lib/policy/errorContextGenerator.d.ts +63 -0
- package/dist/lib/policy/errorContextGenerator.js +302 -0
- package/dist/lib/policy/errors.d.ts +40 -0
- package/dist/lib/policy/errors.js +109 -0
- package/dist/lib/policy/index.d.ts +11 -0
- package/dist/lib/policy/index.js +10 -0
- package/dist/lib/policy/parallelExecutor.d.ts +58 -0
- package/dist/lib/policy/parallelExecutor.js +215 -0
- package/dist/lib/policy/pluginLoader.d.ts +41 -0
- package/dist/lib/policy/pluginLoader.js +220 -0
- package/dist/lib/policy/pluginRegistry.d.ts +14 -0
- package/dist/lib/policy/pluginRegistry.js +69 -0
- package/dist/lib/policy/policyEngine.d.ts +39 -0
- package/dist/lib/policy/policyEngine.js +495 -0
- package/dist/lib/policy/resultAggregator.d.ts +8 -0
- package/dist/lib/policy/resultAggregator.js +138 -0
- package/dist/lib/policy/resultFormatter.d.ts +25 -0
- package/dist/lib/policy/resultFormatter.js +217 -0
- package/dist/lib/policy/types.d.ts +111 -0
- package/dist/lib/policy/types.js +1 -0
- package/dist/lib/policy/validationCache.d.ts +58 -0
- package/dist/lib/policy/validationCache.js +289 -0
- package/dist/lib/rutter.d.ts +7 -2
- package/dist/lib/rutter.js +177 -8
- package/dist/lib/templates/flexible-subchart.js +4 -2
- package/dist/lib/templates/umbrella-chart.js +8 -8
- package/dist/lib/umbrellaRutter.d.ts +1 -1
- package/dist/lib/umbrellaRutter.js +2 -2
- package/dist/lib/validation/inputValidator.d.ts +26 -0
- package/dist/lib/validation/inputValidator.js +176 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- package/package.json +22 -20
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export class PolicyEngineError extends Error {
|
|
2
|
+
constructor(message, code, context) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.code = code;
|
|
5
|
+
this.context = context;
|
|
6
|
+
this.name = 'PolicyEngineError';
|
|
7
|
+
if (Error.captureStackTrace) {
|
|
8
|
+
Error.captureStackTrace(this, PolicyEngineError);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class PluginError extends PolicyEngineError {
|
|
13
|
+
constructor(message, pluginName, context) {
|
|
14
|
+
super(message, 'PLUGIN_ERROR', { ...context, pluginName });
|
|
15
|
+
this.pluginName = pluginName;
|
|
16
|
+
this.name = 'PluginError';
|
|
17
|
+
if (Error.captureStackTrace) {
|
|
18
|
+
Error.captureStackTrace(this, PluginError);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ValidationTimeoutError extends PolicyEngineError {
|
|
23
|
+
constructor(pluginName, timeout) {
|
|
24
|
+
super(`Plugin '${pluginName}' timed out after ${timeout}ms`, 'VALIDATION_TIMEOUT', {
|
|
25
|
+
pluginName,
|
|
26
|
+
timeout,
|
|
27
|
+
});
|
|
28
|
+
this.name = 'ValidationTimeoutError';
|
|
29
|
+
if (Error.captureStackTrace) {
|
|
30
|
+
Error.captureStackTrace(this, ValidationTimeoutError);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class PluginRegistrationError extends PolicyEngineError {
|
|
35
|
+
constructor(message, pluginName, context) {
|
|
36
|
+
super(message, 'PLUGIN_REGISTRATION_ERROR', { ...context, pluginName });
|
|
37
|
+
this.pluginName = pluginName;
|
|
38
|
+
this.name = 'PluginRegistrationError';
|
|
39
|
+
if (Error.captureStackTrace) {
|
|
40
|
+
Error.captureStackTrace(this, PluginRegistrationError);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class PluginConfigurationError extends PolicyEngineError {
|
|
45
|
+
constructor(message, pluginName, configPath, context) {
|
|
46
|
+
super(message, 'PLUGIN_CONFIGURATION_ERROR', { ...context, pluginName, configPath });
|
|
47
|
+
this.pluginName = pluginName;
|
|
48
|
+
this.configPath = configPath;
|
|
49
|
+
this.name = 'PluginConfigurationError';
|
|
50
|
+
if (Error.captureStackTrace) {
|
|
51
|
+
Error.captureStackTrace(this, PluginConfigurationError);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class ValidationOrchestrationError extends PolicyEngineError {
|
|
56
|
+
constructor(message, failedPlugins, context) {
|
|
57
|
+
super(message, 'VALIDATION_ORCHESTRATION_ERROR', { ...context, failedPlugins });
|
|
58
|
+
this.failedPlugins = failedPlugins;
|
|
59
|
+
this.name = 'ValidationOrchestrationError';
|
|
60
|
+
if (Error.captureStackTrace) {
|
|
61
|
+
Error.captureStackTrace(this, ValidationOrchestrationError);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class PluginRetryExhaustedError extends PolicyEngineError {
|
|
66
|
+
constructor(message, pluginName, attempts, lastError, context) {
|
|
67
|
+
super(message, 'PLUGIN_RETRY_EXHAUSTED', {
|
|
68
|
+
...context,
|
|
69
|
+
pluginName,
|
|
70
|
+
attempts,
|
|
71
|
+
lastError: lastError.message,
|
|
72
|
+
});
|
|
73
|
+
this.pluginName = pluginName;
|
|
74
|
+
this.attempts = attempts;
|
|
75
|
+
this.lastError = lastError;
|
|
76
|
+
this.name = 'PluginRetryExhaustedError';
|
|
77
|
+
if (Error.captureStackTrace) {
|
|
78
|
+
Error.captureStackTrace(this, PluginRetryExhaustedError);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export class GracefulDegradationError extends PolicyEngineError {
|
|
83
|
+
constructor(message, degradedPlugins, originalErrors, context) {
|
|
84
|
+
super(message, 'GRACEFUL_DEGRADATION', {
|
|
85
|
+
...context,
|
|
86
|
+
degradedPlugins,
|
|
87
|
+
errorCount: originalErrors.length,
|
|
88
|
+
});
|
|
89
|
+
this.degradedPlugins = degradedPlugins;
|
|
90
|
+
this.originalErrors = originalErrors;
|
|
91
|
+
this.name = 'GracefulDegradationError';
|
|
92
|
+
if (Error.captureStackTrace) {
|
|
93
|
+
Error.captureStackTrace(this, GracefulDegradationError);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export class PolicyValidationError extends PolicyEngineError {
|
|
98
|
+
constructor(message, field, context) {
|
|
99
|
+
super(message, 'POLICY_VALIDATION_ERROR', {
|
|
100
|
+
...context,
|
|
101
|
+
field,
|
|
102
|
+
});
|
|
103
|
+
this.field = field;
|
|
104
|
+
this.name = 'PolicyValidationError';
|
|
105
|
+
if (Error.captureStackTrace) {
|
|
106
|
+
Error.captureStackTrace(this, PolicyValidationError);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { PolicyEngine } from './policyEngine.js';
|
|
2
|
+
export { PluginRegistry } from './pluginRegistry.js';
|
|
3
|
+
export { ConfigurationLoader } from './configurationLoader.js';
|
|
4
|
+
export { PluginLoader } from './pluginLoader.js';
|
|
5
|
+
export { ValidationCache, generateManifestHash, generatePluginHash, type CacheEntry, type CacheOptions, type CacheStats, } from './validationCache.js';
|
|
6
|
+
export { ParallelExecutor, calculateOptimalConcurrency, type ParallelExecutionOptions, type PluginExecutionResult, type ParallelExecutionStats, } from './parallelExecutor.js';
|
|
7
|
+
export { aggregateResults, generateResultSummary, filterViolationsBySeverity, groupViolationsByPlugin, sortViolationsBySeverity, createEmptyResult, mergeViolationContexts, } from './resultAggregator.js';
|
|
8
|
+
export { DefaultResultFormatter, JsonResultFormatter, CompactResultFormatter, GitHubActionsResultFormatter, SarifResultFormatter, createFormatter, getAvailableFormatters, } from './resultFormatter.js';
|
|
9
|
+
export { ErrorContextGenerator, type ErrorContext } from './errorContextGenerator.js';
|
|
10
|
+
export type { PolicyEngine as IPolicyEngine, PolicyPlugin, PolicyEngineOptions, PolicyResult, PolicyViolation, PolicyWarning, ValidationContext, ValidationMetadata, JSONSchema, PluginMetadata, ResultFormatter, ResultSummary, RetryConfig, ConfigurationLoaderOptions, } from './types.js';
|
|
11
|
+
export { PolicyEngineError, PluginError, ValidationTimeoutError, PluginRegistrationError, PluginConfigurationError, ValidationOrchestrationError, PluginRetryExhaustedError, GracefulDegradationError, } from './errors.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { PolicyEngine } from './policyEngine.js';
|
|
2
|
+
export { PluginRegistry } from './pluginRegistry.js';
|
|
3
|
+
export { ConfigurationLoader } from './configurationLoader.js';
|
|
4
|
+
export { PluginLoader } from './pluginLoader.js';
|
|
5
|
+
export { ValidationCache, generateManifestHash, generatePluginHash, } from './validationCache.js';
|
|
6
|
+
export { ParallelExecutor, calculateOptimalConcurrency, } from './parallelExecutor.js';
|
|
7
|
+
export { aggregateResults, generateResultSummary, filterViolationsBySeverity, groupViolationsByPlugin, sortViolationsBySeverity, createEmptyResult, mergeViolationContexts, } from './resultAggregator.js';
|
|
8
|
+
export { DefaultResultFormatter, JsonResultFormatter, CompactResultFormatter, GitHubActionsResultFormatter, SarifResultFormatter, createFormatter, getAvailableFormatters, } from './resultFormatter.js';
|
|
9
|
+
export { ErrorContextGenerator } from './errorContextGenerator.js';
|
|
10
|
+
export { PolicyEngineError, PluginError, ValidationTimeoutError, PluginRegistrationError, PluginConfigurationError, ValidationOrchestrationError, PluginRetryExhaustedError, GracefulDegradationError, } from './errors.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PolicyPlugin, PolicyViolation, ValidationContext } from './types.js';
|
|
2
|
+
export interface ParallelExecutionOptions {
|
|
3
|
+
maxConcurrency?: number;
|
|
4
|
+
useWorkerThreads?: boolean;
|
|
5
|
+
pluginTimeout?: number;
|
|
6
|
+
failFast?: boolean;
|
|
7
|
+
resourceLimits?: {
|
|
8
|
+
maxMemoryMB?: number;
|
|
9
|
+
maxCpuTimeMs?: number;
|
|
10
|
+
};
|
|
11
|
+
priorityConfig?: {
|
|
12
|
+
highPriority?: string[];
|
|
13
|
+
lowPriority?: string[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface PluginExecutionResult {
|
|
17
|
+
plugin: PolicyPlugin;
|
|
18
|
+
violations: PolicyViolation[];
|
|
19
|
+
executionTime: number;
|
|
20
|
+
error?: Error;
|
|
21
|
+
resourceUsage?: {
|
|
22
|
+
memoryUsageMB: number;
|
|
23
|
+
cpuTimeMs: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface ParallelExecutionStats {
|
|
27
|
+
totalExecutionTime: number;
|
|
28
|
+
averagePluginTime: number;
|
|
29
|
+
maxPluginTime: number;
|
|
30
|
+
parallelPluginCount: number;
|
|
31
|
+
concurrencyUtilization: number;
|
|
32
|
+
resourceUsage: {
|
|
33
|
+
totalMemoryMB: number;
|
|
34
|
+
totalCpuTimeMs: number;
|
|
35
|
+
peakConcurrentPlugins: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare class ParallelExecutor {
|
|
39
|
+
private readonly options;
|
|
40
|
+
private readonly logger;
|
|
41
|
+
private activeExecutions;
|
|
42
|
+
private executionQueue;
|
|
43
|
+
constructor(options?: ParallelExecutionOptions);
|
|
44
|
+
executePlugins(plugins: PolicyPlugin[], manifests: unknown[], validationContext: ValidationContext): Promise<{
|
|
45
|
+
results: PluginExecutionResult[];
|
|
46
|
+
stats: ParallelExecutionStats;
|
|
47
|
+
}>;
|
|
48
|
+
private executePlugin;
|
|
49
|
+
private executeWithTimeout;
|
|
50
|
+
private executeConcurrently;
|
|
51
|
+
private sortPluginsByPriority;
|
|
52
|
+
private calculateStats;
|
|
53
|
+
}
|
|
54
|
+
export declare function calculateOptimalConcurrency(pluginCount: number, systemInfo?: {
|
|
55
|
+
cpuCount?: number;
|
|
56
|
+
availableMemoryMB?: number;
|
|
57
|
+
isContainerized?: boolean;
|
|
58
|
+
}): number;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { cpus, totalmem } from 'os';
|
|
2
|
+
import { createLogger } from '../utils/logger.js';
|
|
3
|
+
import { ValidationTimeoutError } from './errors.js';
|
|
4
|
+
const UNKNOWN_ERROR_MESSAGE = 'Unknown error';
|
|
5
|
+
const DEFAULT_PARALLEL_OPTIONS = {
|
|
6
|
+
maxConcurrency: Math.max(2, Math.min(8, cpus().length)),
|
|
7
|
+
useWorkerThreads: false,
|
|
8
|
+
pluginTimeout: 5000,
|
|
9
|
+
failFast: false,
|
|
10
|
+
};
|
|
11
|
+
export class ParallelExecutor {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.activeExecutions = 0;
|
|
14
|
+
this.executionQueue = [];
|
|
15
|
+
this.options = { ...DEFAULT_PARALLEL_OPTIONS, ...options };
|
|
16
|
+
this.logger = createLogger('parallel-executor');
|
|
17
|
+
this.logger.debug('ParallelExecutor initialized', {
|
|
18
|
+
maxConcurrency: this.options.maxConcurrency,
|
|
19
|
+
useWorkerThreads: this.options.useWorkerThreads,
|
|
20
|
+
pluginTimeout: this.options.pluginTimeout,
|
|
21
|
+
operation: 'parallel_executor_init',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async executePlugins(plugins, manifests, validationContext) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
this.logger.info('Starting parallel plugin execution', {
|
|
27
|
+
pluginCount: plugins.length,
|
|
28
|
+
maxConcurrency: this.options.maxConcurrency,
|
|
29
|
+
manifestCount: manifests.length,
|
|
30
|
+
operation: 'parallel_execution_start',
|
|
31
|
+
});
|
|
32
|
+
const sortedPlugins = this.sortPluginsByPriority(plugins);
|
|
33
|
+
const executionTasks = sortedPlugins.map((plugin) => () => this.executePlugin(plugin, manifests, validationContext));
|
|
34
|
+
const results = await this.executeConcurrently(executionTasks);
|
|
35
|
+
const totalExecutionTime = Date.now() - startTime;
|
|
36
|
+
const stats = this.calculateStats(results, totalExecutionTime);
|
|
37
|
+
this.logger.info('Parallel plugin execution completed', {
|
|
38
|
+
pluginCount: plugins.length,
|
|
39
|
+
totalExecutionTime,
|
|
40
|
+
averagePluginTime: stats.averagePluginTime,
|
|
41
|
+
concurrencyUtilization: stats.concurrencyUtilization,
|
|
42
|
+
operation: 'parallel_execution_complete',
|
|
43
|
+
});
|
|
44
|
+
return { results, stats };
|
|
45
|
+
}
|
|
46
|
+
async executePlugin(plugin, manifests, validationContext) {
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
const startMemory = process.memoryUsage().heapUsed;
|
|
49
|
+
this.logger.debug('Starting plugin execution', {
|
|
50
|
+
pluginName: plugin.name,
|
|
51
|
+
pluginVersion: plugin.version,
|
|
52
|
+
operation: 'plugin_execution_start',
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
const violations = await this.executeWithTimeout(plugin, manifests, validationContext, this.options.pluginTimeout);
|
|
56
|
+
const executionTime = Date.now() - startTime;
|
|
57
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
58
|
+
const memoryUsageMB = Math.max(0, endMemory - startMemory) / (1024 * 1024);
|
|
59
|
+
const result = {
|
|
60
|
+
plugin,
|
|
61
|
+
violations,
|
|
62
|
+
executionTime,
|
|
63
|
+
resourceUsage: {
|
|
64
|
+
memoryUsageMB,
|
|
65
|
+
cpuTimeMs: executionTime,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
this.logger.debug('Plugin execution completed successfully', {
|
|
69
|
+
pluginName: plugin.name,
|
|
70
|
+
executionTime,
|
|
71
|
+
violationCount: violations.length,
|
|
72
|
+
memoryUsageMB: memoryUsageMB.toFixed(2),
|
|
73
|
+
operation: 'plugin_execution_success',
|
|
74
|
+
});
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const executionTime = Date.now() - startTime;
|
|
79
|
+
const endMemory = process.memoryUsage().heapUsed;
|
|
80
|
+
const memoryUsageMB = Math.max(0, endMemory - startMemory) / (1024 * 1024);
|
|
81
|
+
this.logger.warn('Plugin execution failed', {
|
|
82
|
+
pluginName: plugin.name,
|
|
83
|
+
executionTime,
|
|
84
|
+
error: error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE,
|
|
85
|
+
memoryUsageMB: memoryUsageMB.toFixed(2),
|
|
86
|
+
operation: 'plugin_execution_error',
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
plugin,
|
|
90
|
+
violations: [],
|
|
91
|
+
executionTime,
|
|
92
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
93
|
+
resourceUsage: {
|
|
94
|
+
memoryUsageMB,
|
|
95
|
+
cpuTimeMs: executionTime,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async executeWithTimeout(plugin, manifests, validationContext, timeout) {
|
|
101
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
102
|
+
globalThis.setTimeout(() => {
|
|
103
|
+
reject(new ValidationTimeoutError(plugin.name, timeout));
|
|
104
|
+
}, timeout);
|
|
105
|
+
});
|
|
106
|
+
const validationPromise = plugin.validate(manifests, validationContext);
|
|
107
|
+
return Promise.race([validationPromise, timeoutPromise]);
|
|
108
|
+
}
|
|
109
|
+
async executeConcurrently(tasks) {
|
|
110
|
+
const results = [];
|
|
111
|
+
const executing = [];
|
|
112
|
+
let taskIndex = 0;
|
|
113
|
+
let peakConcurrency = 0;
|
|
114
|
+
const executeNext = async () => {
|
|
115
|
+
if (taskIndex >= tasks.length) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const currentTaskIndex = taskIndex++;
|
|
119
|
+
const task = tasks[currentTaskIndex];
|
|
120
|
+
if (!task) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.activeExecutions++;
|
|
124
|
+
peakConcurrency = Math.max(peakConcurrency, this.activeExecutions);
|
|
125
|
+
try {
|
|
126
|
+
const result = await task();
|
|
127
|
+
results[currentTaskIndex] = result;
|
|
128
|
+
if (this.options.failFast && result.error) {
|
|
129
|
+
this.logger.debug('Fail-fast triggered, stopping execution', {
|
|
130
|
+
pluginName: result.plugin.name,
|
|
131
|
+
error: result.error.message,
|
|
132
|
+
operation: 'parallel_execution_fail_fast',
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.logger.error('Unexpected error in parallel execution', {
|
|
139
|
+
taskIndex: currentTaskIndex,
|
|
140
|
+
error: error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE,
|
|
141
|
+
operation: 'parallel_execution_unexpected_error',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
this.activeExecutions--;
|
|
146
|
+
}
|
|
147
|
+
if (!this.options.failFast || !results[currentTaskIndex]?.error) {
|
|
148
|
+
await executeNext();
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const maxConcurrency = this.options.maxConcurrency;
|
|
152
|
+
for (let i = 0; i < Math.min(maxConcurrency, tasks.length); i++) {
|
|
153
|
+
executing.push(executeNext());
|
|
154
|
+
}
|
|
155
|
+
await Promise.all(executing);
|
|
156
|
+
this.logger.debug('Concurrent execution completed', {
|
|
157
|
+
totalTasks: tasks.length,
|
|
158
|
+
completedTasks: results.filter((r) => r).length,
|
|
159
|
+
peakConcurrency,
|
|
160
|
+
operation: 'concurrent_execution_complete',
|
|
161
|
+
});
|
|
162
|
+
return results.filter((r) => r);
|
|
163
|
+
}
|
|
164
|
+
sortPluginsByPriority(plugins) {
|
|
165
|
+
if (!this.options.priorityConfig) {
|
|
166
|
+
return [...plugins];
|
|
167
|
+
}
|
|
168
|
+
const { highPriority = [], lowPriority = [] } = this.options.priorityConfig;
|
|
169
|
+
const highPriorityPlugins = plugins.filter((p) => highPriority.includes(p.name));
|
|
170
|
+
const normalPriorityPlugins = plugins.filter((p) => !highPriority.includes(p.name) && !lowPriority.includes(p.name));
|
|
171
|
+
const lowPriorityPlugins = plugins.filter((p) => lowPriority.includes(p.name));
|
|
172
|
+
const sortedPlugins = [...highPriorityPlugins, ...normalPriorityPlugins, ...lowPriorityPlugins];
|
|
173
|
+
this.logger.debug('Plugins sorted by priority', {
|
|
174
|
+
totalPlugins: plugins.length,
|
|
175
|
+
highPriorityCount: highPriorityPlugins.length,
|
|
176
|
+
normalPriorityCount: normalPriorityPlugins.length,
|
|
177
|
+
lowPriorityCount: lowPriorityPlugins.length,
|
|
178
|
+
operation: 'plugin_priority_sort',
|
|
179
|
+
});
|
|
180
|
+
return sortedPlugins;
|
|
181
|
+
}
|
|
182
|
+
calculateStats(results, totalExecutionTime) {
|
|
183
|
+
const executionTimes = results.map((r) => r.executionTime);
|
|
184
|
+
const averagePluginTime = executionTimes.reduce((sum, time) => sum + time, 0) / results.length;
|
|
185
|
+
const maxPluginTime = Math.max(...executionTimes);
|
|
186
|
+
const totalMemoryMB = results.reduce((sum, r) => sum + (r.resourceUsage?.memoryUsageMB || 0), 0);
|
|
187
|
+
const totalCpuTimeMs = results.reduce((sum, r) => sum + (r.resourceUsage?.cpuTimeMs || 0), 0);
|
|
188
|
+
const theoreticalMinTime = Math.max(...executionTimes);
|
|
189
|
+
const concurrencyUtilization = theoreticalMinTime / totalExecutionTime;
|
|
190
|
+
return {
|
|
191
|
+
totalExecutionTime,
|
|
192
|
+
averagePluginTime,
|
|
193
|
+
maxPluginTime,
|
|
194
|
+
parallelPluginCount: results.length,
|
|
195
|
+
concurrencyUtilization: Math.min(1, concurrencyUtilization),
|
|
196
|
+
resourceUsage: {
|
|
197
|
+
totalMemoryMB,
|
|
198
|
+
totalCpuTimeMs,
|
|
199
|
+
peakConcurrentPlugins: Math.min(this.options.maxConcurrency, results.length),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export function calculateOptimalConcurrency(pluginCount, systemInfo) {
|
|
205
|
+
const cpuCount = systemInfo?.cpuCount || cpus().length;
|
|
206
|
+
const availableMemoryMB = systemInfo?.availableMemoryMB || totalmem() / (1024 * 1024);
|
|
207
|
+
let optimalConcurrency = Math.max(2, Math.min(cpuCount, pluginCount));
|
|
208
|
+
const memoryBasedConcurrency = Math.floor(availableMemoryMB / 100);
|
|
209
|
+
optimalConcurrency = Math.min(optimalConcurrency, memoryBasedConcurrency);
|
|
210
|
+
if (systemInfo?.isContainerized) {
|
|
211
|
+
optimalConcurrency = Math.max(1, Math.floor(optimalConcurrency * 0.75));
|
|
212
|
+
}
|
|
213
|
+
optimalConcurrency = Math.min(optimalConcurrency, 16);
|
|
214
|
+
return optimalConcurrency;
|
|
215
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PolicyPlugin, PluginMetadata } from './types.js';
|
|
2
|
+
export interface PluginLoaderOptions {
|
|
3
|
+
baseDirectory?: string;
|
|
4
|
+
environment?: string;
|
|
5
|
+
validateCompatibility?: boolean;
|
|
6
|
+
supportedKubernetesVersions?: string[];
|
|
7
|
+
registryUrl?: string;
|
|
8
|
+
loadTimeout?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PluginDiscoveryResult {
|
|
11
|
+
packageName: string;
|
|
12
|
+
version: string;
|
|
13
|
+
metadata?: PluginMetadata;
|
|
14
|
+
compatible: boolean;
|
|
15
|
+
compatibilityIssues?: string[];
|
|
16
|
+
source: 'npm' | 'local' | 'registry';
|
|
17
|
+
}
|
|
18
|
+
export interface PluginLoadingResult {
|
|
19
|
+
plugin: PolicyPlugin;
|
|
20
|
+
packageName: string;
|
|
21
|
+
source: 'npm' | 'local' | 'registry';
|
|
22
|
+
loadTime: number;
|
|
23
|
+
validated: boolean;
|
|
24
|
+
warnings?: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare class PluginLoader {
|
|
27
|
+
private readonly options;
|
|
28
|
+
private readonly logger;
|
|
29
|
+
private readonly loadedPlugins;
|
|
30
|
+
constructor(options?: PluginLoaderOptions);
|
|
31
|
+
loadFromPackage(packageName: string, version?: string): Promise<PluginLoadingResult>;
|
|
32
|
+
discoverPlugins(searchPattern?: string): Promise<PluginDiscoveryResult[]>;
|
|
33
|
+
loadMultiplePlugins(packages: string[]): Promise<PluginLoadingResult[]>;
|
|
34
|
+
static validatePluginInterface(plugin: unknown, packageName: string): asserts plugin is PolicyPlugin;
|
|
35
|
+
getLoadedPlugins(): PluginLoadingResult[];
|
|
36
|
+
clearCache(): void;
|
|
37
|
+
private loadModuleWithTimeout;
|
|
38
|
+
private validatePluginInterface;
|
|
39
|
+
private checkCompatibility;
|
|
40
|
+
private shouldLoadInEnvironment;
|
|
41
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createLogger } from '../utils/logger.js';
|
|
2
|
+
import { PluginError, PluginRegistrationError } from './errors.js';
|
|
3
|
+
const UNKNOWN_ERROR_MESSAGE = 'Unknown error';
|
|
4
|
+
const DEFAULT_LOADER_OPTIONS = {
|
|
5
|
+
baseDirectory: process.cwd(),
|
|
6
|
+
environment: 'development',
|
|
7
|
+
validateCompatibility: true,
|
|
8
|
+
supportedKubernetesVersions: ['1.25', '1.26', '1.27', '1.28', '1.29'],
|
|
9
|
+
registryUrl: 'https://registry.npmjs.org',
|
|
10
|
+
loadTimeout: 30000,
|
|
11
|
+
};
|
|
12
|
+
export class PluginLoader {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.loadedPlugins = new Map();
|
|
15
|
+
this.options = { ...DEFAULT_LOADER_OPTIONS, ...options };
|
|
16
|
+
this.logger = createLogger('policy-plugin-loader');
|
|
17
|
+
this.logger.debug('PluginLoader initialized', {
|
|
18
|
+
options: this.options,
|
|
19
|
+
operation: 'plugin_loader_init',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async loadFromPackage(packageName, version) {
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
this.logger.info('Loading plugin from package', {
|
|
25
|
+
packageName,
|
|
26
|
+
version,
|
|
27
|
+
operation: 'load_from_package',
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const cacheKey = `${packageName}@${version || 'latest'}`;
|
|
31
|
+
if (this.loadedPlugins.has(cacheKey)) {
|
|
32
|
+
const cached = this.loadedPlugins.get(cacheKey);
|
|
33
|
+
this.logger.debug('Plugin loaded from cache', {
|
|
34
|
+
packageName,
|
|
35
|
+
cacheKey,
|
|
36
|
+
operation: 'load_from_cache',
|
|
37
|
+
});
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
const modulePath = version ? `${packageName}@${version}` : packageName;
|
|
41
|
+
const plugin = await this.loadModuleWithTimeout(modulePath);
|
|
42
|
+
this.validatePluginInterface(plugin, packageName);
|
|
43
|
+
const warnings = [];
|
|
44
|
+
if (this.options.validateCompatibility) {
|
|
45
|
+
const compatibilityIssues = this.checkCompatibility(plugin);
|
|
46
|
+
warnings.push(...compatibilityIssues);
|
|
47
|
+
}
|
|
48
|
+
if (!this.shouldLoadInEnvironment(plugin)) {
|
|
49
|
+
throw new PluginError(`Plugin '${packageName}' is not enabled for environment '${this.options.environment}'`, packageName);
|
|
50
|
+
}
|
|
51
|
+
const loadTime = Date.now() - startTime;
|
|
52
|
+
const result = {
|
|
53
|
+
plugin,
|
|
54
|
+
packageName,
|
|
55
|
+
source: 'npm',
|
|
56
|
+
loadTime,
|
|
57
|
+
validated: true,
|
|
58
|
+
...(warnings.length > 0 && { warnings }),
|
|
59
|
+
};
|
|
60
|
+
this.loadedPlugins.set(cacheKey, result);
|
|
61
|
+
this.logger.info('Plugin loaded successfully', {
|
|
62
|
+
packageName,
|
|
63
|
+
pluginName: plugin.name,
|
|
64
|
+
pluginVersion: plugin.version,
|
|
65
|
+
loadTime,
|
|
66
|
+
hasWarnings: warnings.length > 0,
|
|
67
|
+
operation: 'plugin_loaded',
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const loadTime = Date.now() - startTime;
|
|
73
|
+
this.logger.error('Plugin loading failed', {
|
|
74
|
+
packageName,
|
|
75
|
+
version,
|
|
76
|
+
loadTime,
|
|
77
|
+
error: error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE,
|
|
78
|
+
operation: 'plugin_load_failed',
|
|
79
|
+
});
|
|
80
|
+
throw new PluginError(`Failed to load plugin from package '${packageName}': ${error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE}`, packageName, { originalError: error, loadTime });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async discoverPlugins(searchPattern) {
|
|
84
|
+
this.logger.info('Discovering plugins', {
|
|
85
|
+
searchPattern,
|
|
86
|
+
environment: this.options.environment,
|
|
87
|
+
operation: 'discover_plugins',
|
|
88
|
+
});
|
|
89
|
+
const discovered = [];
|
|
90
|
+
try {
|
|
91
|
+
this.logger.info('Plugin discovery completed', {
|
|
92
|
+
discoveredCount: discovered.length,
|
|
93
|
+
operation: 'discovery_complete',
|
|
94
|
+
});
|
|
95
|
+
return discovered;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
this.logger.error('Plugin discovery failed', {
|
|
99
|
+
error: error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE,
|
|
100
|
+
operation: 'discovery_failed',
|
|
101
|
+
});
|
|
102
|
+
throw new PluginError(`Plugin discovery failed: ${error instanceof Error ? error.message : UNKNOWN_ERROR_MESSAGE}`, 'discovery');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async loadMultiplePlugins(packages) {
|
|
106
|
+
this.logger.info('Loading multiple plugins', {
|
|
107
|
+
packageCount: packages.length,
|
|
108
|
+
packages,
|
|
109
|
+
operation: 'load_multiple',
|
|
110
|
+
});
|
|
111
|
+
const results = [];
|
|
112
|
+
const errors = [];
|
|
113
|
+
const loadPromises = packages.map(async (pkg) => {
|
|
114
|
+
try {
|
|
115
|
+
const [packageName, version] = pkg.includes('@') && !pkg.startsWith('@') ? pkg.split('@') : [pkg, undefined];
|
|
116
|
+
return await this.loadFromPackage(packageName, version);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
errors.push({
|
|
120
|
+
package: pkg,
|
|
121
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
122
|
+
});
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const loadResults = await Promise.all(loadPromises);
|
|
127
|
+
for (const result of loadResults) {
|
|
128
|
+
if (result) {
|
|
129
|
+
results.push(result);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (errors.length > 0) {
|
|
133
|
+
this.logger.warn('Some plugins failed to load', {
|
|
134
|
+
successCount: results.length,
|
|
135
|
+
errorCount: errors.length,
|
|
136
|
+
errors: errors.map((e) => ({ package: e.package, error: e.error.message })),
|
|
137
|
+
operation: 'load_multiple_partial_failure',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
this.logger.info('Multiple plugin loading completed', {
|
|
141
|
+
totalPackages: packages.length,
|
|
142
|
+
successCount: results.length,
|
|
143
|
+
errorCount: errors.length,
|
|
144
|
+
operation: 'load_multiple_complete',
|
|
145
|
+
});
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
static validatePluginInterface(plugin, packageName) {
|
|
149
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
150
|
+
throw new PluginRegistrationError(`Plugin from package '${packageName}' must be an object`, packageName);
|
|
151
|
+
}
|
|
152
|
+
const p = plugin;
|
|
153
|
+
if (!p.name || typeof p.name !== 'string') {
|
|
154
|
+
throw new PluginRegistrationError(`Plugin from package '${packageName}' must have a non-empty string name`, packageName);
|
|
155
|
+
}
|
|
156
|
+
if (!p.version || typeof p.version !== 'string') {
|
|
157
|
+
throw new PluginRegistrationError(`Plugin from package '${packageName}' must have a non-empty string version`, packageName);
|
|
158
|
+
}
|
|
159
|
+
if (typeof p.validate !== 'function') {
|
|
160
|
+
throw new PluginRegistrationError(`Plugin from package '${packageName}' must implement validate method`, packageName);
|
|
161
|
+
}
|
|
162
|
+
if (p.description !== undefined && typeof p.description !== 'string') {
|
|
163
|
+
throw new PluginRegistrationError(`Plugin from package '${packageName}' description must be a string`, packageName);
|
|
164
|
+
}
|
|
165
|
+
if (p.configSchema !== undefined && typeof p.configSchema !== 'object') {
|
|
166
|
+
throw new PluginRegistrationError(`Plugin from package '${packageName}' configSchema must be an object`, packageName);
|
|
167
|
+
}
|
|
168
|
+
if (p.metadata !== undefined && typeof p.metadata !== 'object') {
|
|
169
|
+
throw new PluginRegistrationError(`Plugin from package '${packageName}' metadata must be an object`, packageName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
getLoadedPlugins() {
|
|
173
|
+
return Array.from(this.loadedPlugins.values());
|
|
174
|
+
}
|
|
175
|
+
clearCache() {
|
|
176
|
+
this.loadedPlugins.clear();
|
|
177
|
+
this.logger.debug('Plugin cache cleared', {
|
|
178
|
+
operation: 'cache_cleared',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async loadModuleWithTimeout(modulePath) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const timeout = globalThis.setTimeout(() => {
|
|
184
|
+
reject(new Error(`Plugin loading timed out after ${this.options.loadTimeout}ms`));
|
|
185
|
+
}, this.options.loadTimeout);
|
|
186
|
+
import(modulePath)
|
|
187
|
+
.then((module) => {
|
|
188
|
+
globalThis.clearTimeout(timeout);
|
|
189
|
+
const plugin = module.default || module;
|
|
190
|
+
if (!plugin) {
|
|
191
|
+
reject(new Error('Plugin module does not export a plugin'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
resolve(plugin);
|
|
195
|
+
})
|
|
196
|
+
.catch((error) => {
|
|
197
|
+
globalThis.clearTimeout(timeout);
|
|
198
|
+
reject(error);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
validatePluginInterface(plugin, packageName) {
|
|
203
|
+
PluginLoader.validatePluginInterface(plugin, packageName);
|
|
204
|
+
}
|
|
205
|
+
checkCompatibility(plugin) {
|
|
206
|
+
const issues = [];
|
|
207
|
+
if (plugin.metadata?.kubernetesVersions) {
|
|
208
|
+
const supportedVersions = new Set(this.options.supportedKubernetesVersions);
|
|
209
|
+
const pluginVersions = plugin.metadata.kubernetesVersions;
|
|
210
|
+
const hasCompatibleVersion = pluginVersions.some((version) => supportedVersions.has(version));
|
|
211
|
+
if (!hasCompatibleVersion) {
|
|
212
|
+
issues.push(`Plugin requires Kubernetes versions [${pluginVersions.join(', ')}] but only [${this.options.supportedKubernetesVersions.join(', ')}] are supported`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return issues;
|
|
216
|
+
}
|
|
217
|
+
shouldLoadInEnvironment(_plugin) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PolicyPlugin } from './types.js';
|
|
2
|
+
export declare class PluginRegistry {
|
|
3
|
+
private plugins;
|
|
4
|
+
private pluginConfigs;
|
|
5
|
+
register(plugin: PolicyPlugin, config?: unknown): void;
|
|
6
|
+
getPlugin(name: string): PolicyPlugin | undefined;
|
|
7
|
+
getAllPlugins(): PolicyPlugin[];
|
|
8
|
+
getPluginConfig(pluginName: string): unknown;
|
|
9
|
+
hasPlugin(name: string): boolean;
|
|
10
|
+
getPluginCount(): number;
|
|
11
|
+
unregister(name: string): boolean;
|
|
12
|
+
clear(): void;
|
|
13
|
+
private validatePlugin;
|
|
14
|
+
}
|