provisioning-platform/prov-ecosystem/crates/gitops/ARCHITECTURE.md

309 lines
11 KiB
Markdown

# GitOps Module Architecture
**Objective:** Event-driven GitOps orchestration with adaptive execution based on available capabilities.
---
## Core Design Principles
1. **Single Entry Point**: All external access through `GitOpsEngine`
2. **Minimal Public API**: Only 3 public types exposed to consumers
3. **Internal Coupling Acceptable**: Internal modules can depend on each other; this doesn't affect external users
4. **Capability-Driven Execution**: Choose HOW to execute based on WHAT'S available
---
## Module Tiers
### Core Tier (No Dependencies)
```
error.rs → Error types and result type
config.rs → Configuration structures
```
**Purpose**: Foundational types used by all other modules.
### Event Tier
```
event/ → Event definitions and sources
├─ Depends on: error
└─ Used by: engine, flow
```
**Responsibility**:
- Define event sources (Git, webhooks, alerts, schedules, health checks, n8n, manual)
- Provide event matching interface
- Broadcast events through async channels
### Provider Tier
```
provider/ → Git provider abstractions
├─ Depends on: error
└─ Used by: integration, event handlers
```
**Responsibility**:
- Unified interface for GitHub, GitLab, Gitea, Forgejo
- Repository operations, webhook registration
- Event parsing from git providers
### Rule Tier
```
rule/ → Rule definition and registry
├─ Depends on: error, serialization
└─ Used by: engine, flow, executor, generator
```
**Responsibility**:
- Define rule structure (When/Then clauses)
- Rule parsing from YAML/TOML/KCL
- Rule matching against events
### Environment Tier
```
environment/ → Capability detection
├─ Depends on: error, observability
└─ Used by: flow, engine
```
**Responsibility**:
- Auto-detect available deployment tools
- Kubernetes (kubeconfig, API access)
- Docker/Podman (daemon socket)
- ArgoCD (API endpoint)
- Flux (CRDs in cluster)
- systemd (service support)
- n8n (workflow API)
- Prometheus (metrics endpoint)
- Maintain capability cache
- Provide refresh mechanism
### Executor Tier (Internal)
```
executor/ → Action execution implementations
├─ Depends on: error, rule
└─ Used by: flow, engine (indirectly through flow)
```
**Responsibility**:
- Implement execution for each tool (ArgoCD, Flux, Kubernetes, Docker, systemd, n8n, script)
- Translate rule actions to tool-specific commands
- Handle execution results
### Generator Tier (Internal)
```
generator/ → Configuration generation
├─ Depends on: error, rule
└─ Used by: executor, flow (indirectly)
```
**Responsibility**:
- Generate tool-specific configurations from rules
- Support output formats: Kubernetes YAML, ArgoCD CRDs, Flux CRDs, Docker Compose, systemd units, CI/CD workflows
- Validate generated configurations
### Flow Tier (Internal)
```
flow/ → Execution flow resolution
├─ Depends on: error, rule, environment, executor
└─ Used by: engine
```
**Responsibility**:
- Map actions to available executors
- Define execution fallback chains
- deploy: ArgoCD → Flux → Kubernetes → Docker → systemd
- rollback: Kubernetes → Docker → systemd
- notify: n8n → webhook
- etc.
- Select primary and fallback executors
### Orchestration Tier
```
engine/ → Central coordinator
├─ Depends on: error, rule, environment, flow
└─ Public entry point for all external access
```
**Responsibility**:
- Initialize the GitOps system
- Load and validate rules
- Detect environment capabilities
- Coordinate rule execution
- Main event loop (listens for events, matches rules, executes actions)
### Integration Tier
```
integration/ → External integrations
├─ Depends on: config, error, (optional) other modules
└─ Used by: external code, optional features
```
**Responsibility**:
- Syntaxis integration for configuration management
- Custom integrations for specific tools
- Optional extensions
---
## Dependency Graph
```
┌─────────────────────────────────────────────────────────┐
│ External Users │
└──────────────────────────┬──────────────────────────────┘
(public API only)
┌──────────────┐
│ engine │
└──────┬───────┘
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌──────────────┐
│ rule │ │ environment │ │ flow │
└────┬────┘ └─────────────┘ └──────┬───────┘
│ │
│ ┌────────┼────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┬────────┬────────┐
│ event │ │executor │ query │provider│
└─────────┘ └────────┬┘ └────────┘
┌──────────┐
│generator │
└──────────┘
┌─────────────────────────────────────────────────┐
│ Core (no internal dependencies) │
│ error.rs, config.rs, deps/mod.rs │
└─────────────────────────────────────────────────┘
```
---
## Internal Coupling Analysis
**Current State**: `gitops::mod` shows 25 internal dependencies
**Why This Is Acceptable**:
1. **External Users See Only 3 Public Types**:
- `GitOpsEngine` - main entry point
- `GitOpsError`/`GitOpsResult` - error handling
- `Config` - configuration
2. **Internal Modules Can Depend on Each Other**:
- `flow` depends on `rule`, `environment`, `executor` - this is necessary for its responsibility
- `event` depends on `rule` for matching - expected
- `executor` and `generator` both use `rule` - appropriate
3. **Coupling Doesn't Propagate to External API**:
- External code doesn't need to know about flow, executor, generator, environment
- All access goes through `GitOpsEngine`
4. **Implementation Details Are Marked `pub(crate)`**:
- executor, generator, flow, event (mostly), provider, rule - all internal
- This prevents external code from depending on implementation details
- Makes refactoring easier in the future
---
## Public API Surface
The intentionally minimal public API:
```rust
// Stable external interface
pub use engine::GitOpsEngine; // Main entry point
pub use error::{GitOpsError, GitOpsResult}; // Error handling
pub use config::Config; // Configuration
// Everything else is internal
pub(crate) mod event; // Implementation detail
pub(crate) mod rule; // Implementation detail
pub(crate) mod environment; // Implementation detail
pub(crate) mod executor; // Implementation detail
pub(crate) mod generator; // Implementation detail
pub(crate) mod flow; // Implementation detail
pub mod provider; // Interface but limited use
pub mod integration; // Optional extensions
```
---
## Usage Pattern
For external users:
```rust
// Step 1: Load rules
let rules = RuleRegistry::from_yaml("rules.yaml")?;
// Step 2: Create engine (auto-detects environment)
let engine = GitOpsEngine::new(rules).await?;
// Step 3: Run the engine
engine.run().await?;
```
No need to know about flow, executor, generator, environment, event internals.
---
## Future Optimizations
If internal coupling becomes problematic, consider:
1. **Mediator Pattern**: Central event bus for module communication
- Modules send commands/queries to mediator
- Reduces direct module-to-module dependencies
- Trade-off: More complex, requires routing layer
2. **Command/Query Separation**: Explicit command handlers
- `ExecuteCommand` → flow resolves → executor runs
- `QueryCapabilities` → environment responds
- More structured, but more boilerplate
3. **Layer Enforcement**: Strict dependency constraints
- Tier N modules can only depend on Tier N-1
- Prevents some current dependencies (e.g., flow → executor)
- More rigid architecture
---
## Testing Strategy
1. **Unit Tests**: Test each module in isolation with mocks
2. **Integration Tests**: Test complete flow from event to execution
3. **End-to-End Tests**: Full engine lifecycle with real rules
Module mocking strategy:
- External integration points (git providers, executors) are traits
- Use mockall for creating test implementations
- Internal modules use pub(crate) so tests can access them
---
## Coupling Metrics
| Metric | Target | Current | Status |
|--------|--------|---------|--------|
| External API Size | < 5 types | 3 types | Optimal |
| Public Module Count | < 4 | 3 (engine, provider, integration) | Good |
| Module Visibility | All internal use pub(crate) | Most do | Good |
| Circular Dependencies | 0 | 0 | Good |
| External Coupling | Minimal | GitOpsEngine only | Good |
---
## Notes
- The 25 internal dependencies reported by `cargo coupling` are intentional and don't affect external users
- Making all modules `pub(crate)` provides clear API surface without forcing a major internal refactoring
- Future versions can implement mediator pattern if internal complexity grows
- Current design is pragmatic balance between simplicity and decoupling