11 KiB
GitOps Module Architecture
Objective: Event-driven GitOps orchestration with adaptive execution based on available capabilities.
Core Design Principles
- Single Entry Point: All external access through
GitOpsEngine - Minimal Public API: Only 3 public types exposed to consumers
- Internal Coupling Acceptable: Internal modules can depend on each other; this doesn't affect external users
- 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:
-
External Users See Only 3 Public Types:
GitOpsEngine- main entry pointGitOpsError/GitOpsResult- error handlingConfig- configuration
-
Internal Modules Can Depend on Each Other:
flowdepends onrule,environment,executor- this is necessary for its responsibilityeventdepends onrulefor matching - expectedexecutorandgeneratorboth userule- appropriate
-
Coupling Doesn't Propagate to External API:
- External code doesn't need to know about flow, executor, generator, environment
- All access goes through
GitOpsEngine
-
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:
// 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:
// 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:
-
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
-
Command/Query Separation: Explicit command handlers
ExecuteCommand→ flow resolves → executor runsQueryCapabilities→ environment responds- More structured, but more boilerplate
-
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
- Unit Tests: Test each module in isolation with mocks
- Integration Tests: Test complete flow from event to execution
- 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 couplingare 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