release orchestrator pivot, architecture and planning
This commit is contained in:
629
docs/modules/release-orchestrator/modules/plugin-system.md
Normal file
629
docs/modules/release-orchestrator/modules/plugin-system.md
Normal file
@@ -0,0 +1,629 @@
|
||||
# PLUGIN: Plugin Infrastructure
|
||||
|
||||
**Purpose**: Extensible plugin system for integrations, steps, and custom functionality.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PLUGIN ARCHITECTURE │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PLUGIN REGISTRY │ │
|
||||
│ │ │ │
|
||||
│ │ - Plugin discovery and versioning │ │
|
||||
│ │ - Manifest validation │ │
|
||||
│ │ - Dependency resolution │ │
|
||||
│ └──────────────────────────────┬──────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PLUGIN LOADER │ │
|
||||
│ │ │ │
|
||||
│ │ - Lifecycle management (load, start, stop, unload) │ │
|
||||
│ │ - Health monitoring │ │
|
||||
│ │ - Hot reload support │ │
|
||||
│ └──────────────────────────────┬──────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PLUGIN SANDBOX │ │
|
||||
│ │ │ │
|
||||
│ │ - Process isolation │ │
|
||||
│ │ - Resource limits (CPU, memory, network) │ │
|
||||
│ │ - Capability enforcement │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Plugin Types: │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Connector │ │ Step │ │ Gate │ │ Agent │ │
|
||||
│ │ Plugins │ │ Providers │ │ Providers │ │ Plugins │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
### Module: `plugin-registry`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | Plugin discovery; versioning; manifest management |
|
||||
| **Data Entities** | `Plugin`, `PluginManifest`, `PluginVersion` |
|
||||
| **Events Produced** | `plugin.discovered`, `plugin.registered`, `plugin.unregistered` |
|
||||
|
||||
**Plugin Entity**:
|
||||
```typescript
|
||||
interface Plugin {
|
||||
id: UUID;
|
||||
pluginId: string; // "com.example.my-connector"
|
||||
version: string; // "1.2.3"
|
||||
vendor: string;
|
||||
license: string;
|
||||
manifest: PluginManifest;
|
||||
status: PluginStatus;
|
||||
entrypoint: string; // Path to plugin executable/module
|
||||
lastHealthCheck: DateTime;
|
||||
healthMessage: string | null;
|
||||
installedAt: DateTime;
|
||||
updatedAt: DateTime;
|
||||
}
|
||||
|
||||
type PluginStatus =
|
||||
| "discovered" // Found but not loaded
|
||||
| "loaded" // Loaded but not active
|
||||
| "active" // Running and healthy
|
||||
| "stopped" // Manually stopped
|
||||
| "failed" // Failed to load or crashed
|
||||
| "degraded"; // Running but with issues
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Module: `plugin-loader`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | Plugin lifecycle management |
|
||||
| **Dependencies** | `plugin-registry`, `plugin-sandbox` |
|
||||
| **Events Produced** | `plugin.loaded`, `plugin.started`, `plugin.stopped`, `plugin.failed` |
|
||||
|
||||
**Plugin Lifecycle**:
|
||||
```
|
||||
┌──────────────┐
|
||||
│ DISCOVERED │ ──── Plugin found in registry
|
||||
└──────┬───────┘
|
||||
│ load()
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ LOADED │ ──── Plugin validated and prepared
|
||||
└──────┬───────┘
|
||||
│ start()
|
||||
▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ ACTIVE │ ──── │ DEGRADED │ ◄── Health issues
|
||||
└──────┬───────┘ └──────────────┘
|
||||
│ stop() │
|
||||
▼ │
|
||||
┌──────────────┐ │
|
||||
│ STOPPED │ ◄───────────┘ manual stop
|
||||
└──────────────┘
|
||||
|
||||
│ unload()
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ UNLOADED │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**Lifecycle Operations**:
|
||||
```typescript
|
||||
interface PluginLoader {
|
||||
// Discovery
|
||||
discover(): Promise<Plugin[]>;
|
||||
refresh(): Promise<void>;
|
||||
|
||||
// Lifecycle
|
||||
load(pluginId: string): Promise<Plugin>;
|
||||
start(pluginId: string): Promise<void>;
|
||||
stop(pluginId: string): Promise<void>;
|
||||
unload(pluginId: string): Promise<void>;
|
||||
restart(pluginId: string): Promise<void>;
|
||||
|
||||
// Health
|
||||
checkHealth(pluginId: string): Promise<HealthStatus>;
|
||||
getStatus(pluginId: string): Promise<PluginStatus>;
|
||||
|
||||
// Hot reload
|
||||
reload(pluginId: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Module: `plugin-sandbox`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | Isolation; resource limits; security |
|
||||
| **Enforcement** | Process isolation, capability-based security |
|
||||
|
||||
**Sandbox Configuration**:
|
||||
```typescript
|
||||
interface SandboxConfig {
|
||||
// Process isolation
|
||||
processIsolation: boolean; // Run in separate process
|
||||
containerIsolation: boolean; // Run in container
|
||||
|
||||
// Resource limits
|
||||
resourceLimits: {
|
||||
maxMemoryMb: number; // Memory limit
|
||||
maxCpuPercent: number; // CPU limit
|
||||
maxDiskMb: number; // Disk quota
|
||||
maxNetworkBandwidth: number; // Network bandwidth limit
|
||||
};
|
||||
|
||||
// Network restrictions
|
||||
networkPolicy: {
|
||||
allowedHosts: string[]; // Allowed outbound hosts
|
||||
blockedHosts: string[]; // Blocked hosts
|
||||
allowOutbound: boolean; // Allow any outbound
|
||||
};
|
||||
|
||||
// Filesystem restrictions
|
||||
filesystemPolicy: {
|
||||
readOnlyPaths: string[];
|
||||
writablePaths: string[];
|
||||
blockedPaths: string[];
|
||||
};
|
||||
|
||||
// Timeouts
|
||||
timeouts: {
|
||||
initializationMs: number;
|
||||
operationMs: number;
|
||||
shutdownMs: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Capability Enforcement**:
|
||||
```typescript
|
||||
interface PluginCapabilities {
|
||||
// Integration capabilities
|
||||
integrations: {
|
||||
scm: boolean;
|
||||
ci: boolean;
|
||||
registry: boolean;
|
||||
vault: boolean;
|
||||
router: boolean;
|
||||
};
|
||||
|
||||
// Step capabilities
|
||||
steps: {
|
||||
deploy: boolean;
|
||||
gate: boolean;
|
||||
notify: boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
|
||||
// System capabilities
|
||||
system: {
|
||||
network: boolean;
|
||||
filesystem: boolean;
|
||||
secrets: boolean;
|
||||
database: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Module: `plugin-sdk`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | SDK for plugin development |
|
||||
| **Languages** | C#, TypeScript, Go |
|
||||
|
||||
**Plugin SDK Interface**:
|
||||
```typescript
|
||||
// Base plugin interface
|
||||
interface StellaPlugin {
|
||||
// Lifecycle
|
||||
initialize(config: PluginConfig): Promise<void>;
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
dispose(): Promise<void>;
|
||||
|
||||
// Health
|
||||
getHealth(): Promise<HealthStatus>;
|
||||
|
||||
// Metadata
|
||||
getManifest(): PluginManifest;
|
||||
}
|
||||
|
||||
// Connector plugin interface
|
||||
interface ConnectorPlugin extends StellaPlugin {
|
||||
createConnector(config: ConnectorConfig): Promise<Connector>;
|
||||
}
|
||||
|
||||
// Step provider plugin interface
|
||||
interface StepProviderPlugin extends StellaPlugin {
|
||||
getStepTypes(): StepType[];
|
||||
executeStep(
|
||||
stepType: string,
|
||||
config: StepConfig,
|
||||
inputs: StepInputs,
|
||||
context: StepContext
|
||||
): AsyncGenerator<StepEvent>;
|
||||
}
|
||||
|
||||
// Gate provider plugin interface
|
||||
interface GateProviderPlugin extends StellaPlugin {
|
||||
getGateTypes(): GateType[];
|
||||
evaluateGate(
|
||||
gateType: string,
|
||||
config: GateConfig,
|
||||
context: GateContext
|
||||
): Promise<GateResult>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Three-Surface Plugin Model
|
||||
|
||||
Plugins contribute to the system through three distinct surfaces:
|
||||
|
||||
### 1. Manifest Surface (Static)
|
||||
|
||||
The plugin manifest declares:
|
||||
- Plugin identity and version
|
||||
- Required capabilities
|
||||
- Provided integrations/steps/gates
|
||||
- Configuration schema
|
||||
- UI components (optional)
|
||||
|
||||
```yaml
|
||||
# plugin.stella.yaml
|
||||
plugin:
|
||||
id: "com.example.jenkins-connector"
|
||||
version: "1.0.0"
|
||||
vendor: "Example Corp"
|
||||
license: "Apache-2.0"
|
||||
description: "Jenkins CI integration for Stella Ops"
|
||||
|
||||
capabilities:
|
||||
required:
|
||||
- network
|
||||
optional:
|
||||
- secrets
|
||||
|
||||
provides:
|
||||
integrations:
|
||||
- type: "ci.jenkins"
|
||||
displayName: "Jenkins"
|
||||
configSchema: "./schemas/jenkins-config.json"
|
||||
capabilities:
|
||||
- "pipelines"
|
||||
- "builds"
|
||||
- "artifacts"
|
||||
|
||||
steps:
|
||||
- type: "jenkins-trigger"
|
||||
displayName: "Trigger Jenkins Build"
|
||||
category: "integration"
|
||||
configSchema: "./schemas/jenkins-trigger-config.json"
|
||||
inputSchema: "./schemas/jenkins-trigger-input.json"
|
||||
outputSchema: "./schemas/jenkins-trigger-output.json"
|
||||
|
||||
ui:
|
||||
configScreen: "./ui/config.html"
|
||||
icon: "./assets/jenkins-icon.svg"
|
||||
|
||||
dependencies:
|
||||
stellaCore: ">=1.0.0"
|
||||
```
|
||||
|
||||
### 2. Connector Runtime Surface (Dynamic)
|
||||
|
||||
Plugins implement connector interfaces for runtime operations:
|
||||
|
||||
```typescript
|
||||
// Jenkins connector implementation
|
||||
class JenkinsConnector implements CIConnector {
|
||||
private client: JenkinsClient;
|
||||
|
||||
async initialize(config: ConnectorConfig, secrets: SecretHandle[]): Promise<void> {
|
||||
const apiToken = await this.getSecret(secrets, "api_token");
|
||||
this.client = new JenkinsClient({
|
||||
baseUrl: config.endpoint,
|
||||
username: config.username,
|
||||
apiToken: apiToken,
|
||||
});
|
||||
}
|
||||
|
||||
async testConnection(): Promise<ConnectionTestResult> {
|
||||
try {
|
||||
const crumb = await this.client.getCrumb();
|
||||
return { success: true, message: "Connected to Jenkins" };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async listPipelines(): Promise<PipelineInfo[]> {
|
||||
const jobs = await this.client.getJobs();
|
||||
return jobs.map(job => ({
|
||||
id: job.name,
|
||||
name: job.displayName,
|
||||
url: job.url,
|
||||
lastBuild: job.lastBuild?.number,
|
||||
}));
|
||||
}
|
||||
|
||||
async triggerPipeline(pipelineId: string, params: object): Promise<PipelineRun> {
|
||||
const queueItem = await this.client.build(pipelineId, params);
|
||||
return {
|
||||
id: queueItem.id.toString(),
|
||||
pipelineId,
|
||||
status: "queued",
|
||||
startedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
async getPipelineRun(runId: string): Promise<PipelineRun> {
|
||||
const build = await this.client.getBuild(runId);
|
||||
return {
|
||||
id: build.number.toString(),
|
||||
pipelineId: build.job,
|
||||
status: this.mapStatus(build.result),
|
||||
startedAt: new Date(build.timestamp),
|
||||
completedAt: build.result ? new Date(build.timestamp + build.duration) : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Step Provider Surface (Execution)
|
||||
|
||||
Plugins implement step execution logic:
|
||||
|
||||
```typescript
|
||||
// Jenkins trigger step implementation
|
||||
class JenkinsTriggerStep implements StepExecutor {
|
||||
async *execute(
|
||||
config: StepConfig,
|
||||
inputs: StepInputs,
|
||||
context: StepContext
|
||||
): AsyncGenerator<StepEvent> {
|
||||
const connector = await context.getConnector<JenkinsConnector>(config.integrationId);
|
||||
|
||||
yield { type: "log", line: `Triggering Jenkins job: ${config.jobName}` };
|
||||
|
||||
// Trigger build
|
||||
const run = await connector.triggerPipeline(config.jobName, inputs.parameters);
|
||||
yield { type: "output", name: "buildId", value: run.id };
|
||||
yield { type: "log", line: `Build queued: ${run.id}` };
|
||||
|
||||
// Wait for completion if configured
|
||||
if (config.waitForCompletion) {
|
||||
yield { type: "log", line: "Waiting for build to complete..." };
|
||||
|
||||
while (true) {
|
||||
const status = await connector.getPipelineRun(run.id);
|
||||
|
||||
if (status.status === "succeeded") {
|
||||
yield { type: "output", name: "status", value: "succeeded" };
|
||||
yield { type: "result", success: true };
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.status === "failed") {
|
||||
yield { type: "output", name: "status", value: "failed" };
|
||||
yield { type: "result", success: false, message: "Build failed" };
|
||||
return;
|
||||
}
|
||||
|
||||
yield { type: "progress", progress: 50, message: `Build running: ${status.status}` };
|
||||
await sleep(config.pollIntervalSeconds * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
yield { type: "result", success: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
-- Plugins
|
||||
CREATE TABLE release.plugins (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
plugin_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
version VARCHAR(50) NOT NULL,
|
||||
vendor VARCHAR(255) NOT NULL,
|
||||
license VARCHAR(100),
|
||||
manifest JSONB NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'discovered' CHECK (status IN (
|
||||
'discovered', 'loaded', 'active', 'stopped', 'failed', 'degraded'
|
||||
)),
|
||||
entrypoint VARCHAR(500) NOT NULL,
|
||||
last_health_check TIMESTAMPTZ,
|
||||
health_message TEXT,
|
||||
installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_plugins_status ON release.plugins(status);
|
||||
|
||||
-- Plugin Instances (per-tenant configuration)
|
||||
CREATE TABLE release.plugin_instances (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
plugin_id UUID NOT NULL REFERENCES release.plugins(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
config JSONB NOT NULL DEFAULT '{}',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_plugin_instances_tenant ON release.plugin_instances(tenant_id);
|
||||
|
||||
-- Integration types (populated by plugins)
|
||||
CREATE TABLE release.integration_types (
|
||||
id TEXT PRIMARY KEY, -- "scm.github", "ci.jenkins"
|
||||
plugin_id UUID REFERENCES release.plugins(id),
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
icon_url TEXT,
|
||||
config_schema JSONB NOT NULL, -- JSON Schema for config
|
||||
capabilities TEXT[] NOT NULL, -- ["repos", "webhooks", "status"]
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
```yaml
|
||||
# Plugin Registry
|
||||
GET /api/v1/plugins
|
||||
Query: ?status={status}&capability={type}
|
||||
Response: Plugin[]
|
||||
|
||||
GET /api/v1/plugins/{id}
|
||||
Response: Plugin (with manifest)
|
||||
|
||||
POST /api/v1/plugins/{id}/enable
|
||||
Response: Plugin
|
||||
|
||||
POST /api/v1/plugins/{id}/disable
|
||||
Response: Plugin
|
||||
|
||||
GET /api/v1/plugins/{id}/health
|
||||
Response: { status, message, diagnostics[] }
|
||||
|
||||
# Plugin Instances (per-tenant config)
|
||||
POST /api/v1/plugin-instances
|
||||
Body: { pluginId: UUID, config: object }
|
||||
Response: PluginInstance
|
||||
|
||||
GET /api/v1/plugin-instances
|
||||
Response: PluginInstance[]
|
||||
|
||||
PUT /api/v1/plugin-instances/{id}
|
||||
Body: { config: object, enabled: boolean }
|
||||
Response: PluginInstance
|
||||
|
||||
DELETE /api/v1/plugin-instances/{id}
|
||||
Response: { deleted: true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin Security
|
||||
|
||||
### Capability Declaration
|
||||
|
||||
Plugins must declare all required capabilities in their manifest. The system enforces:
|
||||
|
||||
1. **Network Access**: Plugins can only access declared hosts
|
||||
2. **Secret Access**: Plugins receive secrets through controlled injection
|
||||
3. **Database Access**: No direct database access; API only
|
||||
4. **Filesystem Access**: Limited to declared paths
|
||||
|
||||
### Sandbox Enforcement
|
||||
|
||||
```typescript
|
||||
// Plugin execution is sandboxed
|
||||
class PluginSandbox {
|
||||
async execute<T>(
|
||||
plugin: Plugin,
|
||||
operation: () => Promise<T>
|
||||
): Promise<T> {
|
||||
// 1. Verify capabilities
|
||||
this.verifyCapabilities(plugin);
|
||||
|
||||
// 2. Set resource limits
|
||||
const limits = this.getResourceLimits(plugin);
|
||||
await this.applyLimits(limits);
|
||||
|
||||
// 3. Create isolated context
|
||||
const context = await this.createIsolatedContext(plugin);
|
||||
|
||||
try {
|
||||
// 4. Execute with timeout
|
||||
return await this.withTimeout(
|
||||
operation(),
|
||||
plugin.manifest.timeouts.operationMs
|
||||
);
|
||||
} catch (error) {
|
||||
// 5. Log and handle errors
|
||||
await this.handlePluginError(plugin, error);
|
||||
throw error;
|
||||
} finally {
|
||||
// 6. Cleanup
|
||||
await context.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Plugin Failures Cannot Crash Core
|
||||
|
||||
```csharp
|
||||
// Core orchestration is protected from plugin failures
|
||||
public sealed class PromotionDecisionEngine
|
||||
{
|
||||
public async Task<DecisionResult> EvaluateAsync(
|
||||
Promotion promotion,
|
||||
IReadOnlyList<IGateProvider> gates,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var results = new List<GateResult>();
|
||||
|
||||
foreach (var gate in gates)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Plugin provides evaluation logic
|
||||
var result = await gate.EvaluateAsync(promotion, ct);
|
||||
results.Add(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Plugin failure is logged but doesn't crash core
|
||||
_logger.LogError(ex, "Gate {GateType} failed", gate.Type);
|
||||
results.Add(new GateResult
|
||||
{
|
||||
GateType = gate.Type,
|
||||
Status = GateStatus.Failed,
|
||||
Message = $"Gate evaluation failed: {ex.Message}",
|
||||
IsBlocking = gate.IsBlocking,
|
||||
});
|
||||
}
|
||||
|
||||
// Core decides how to aggregate (plugins cannot override)
|
||||
if (results.Last().IsBlocking && _policy.FailFast)
|
||||
break;
|
||||
}
|
||||
|
||||
// Core makes final decision
|
||||
return _decisionAggregator.Aggregate(results);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Module Overview](overview.md)
|
||||
- [Integration Hub](integration-hub.md)
|
||||
- [Workflow Engine](workflow-engine.md)
|
||||
- [Connector Interface](../integrations/connectors.md)
|
||||
Reference in New Issue
Block a user