Architecture #
Quad-Ops is designed with cross-platform support at its core, using a domain-driven architecture that separates platform-agnostic service definitions from platform-specific implementations.
Cross-Platform Design #
Quad-Ops supports multiple service management platforms through a clean abstraction layer:
- Linux: systemd + Podman Quadlet
- macOS: launchd (planned)
- Future: Additional platforms can be added without changing core logic
The platform is automatically detected at runtime, and the appropriate backend is used for service management.
Architecture Overview #
graph TB
subgraph "CLI Layer"
CMD[Commands<br/>daemon, sync, up, down]
end
subgraph "Component Layer"
COMPOSE[Compose Processor<br/>Parse Docker Compose]
MODELS[Service Models<br/>Platform-Agnostic Specs]
REPO[Repository<br/>Git Sync + Storage]
CONFIG[Configuration<br/>Settings Management]
end
subgraph "Platform Adapters"
SYSTEMD[Systemd Adapter<br/>Linux]
LAUNCHD[Launchd Adapter<br/>macOS - Planned]
end
subgraph "External Systems"
GIT[Git Repositories]
SYS[systemd/launchd]
POD[Podman/Docker]
end
CMD --> COMPOSE
CMD --> MODELS
CMD --> REPO
CMD --> CONFIG
COMPOSE --> MODELS
MODELS --> SYSTEMD
MODELS --> LAUNCHD
REPO --> GIT
SYSTEMD --> SYS
SYSTEMD --> POD
LAUNCHD --> SYS
Core Concepts #
1. Platform-Agnostic Service Models #
The heart of quad-ops is the service.Spec domain model, which represents container services independent of any platform:
type Spec struct {
Name string
Description string
Container Container
Volumes []Volume
Networks []Network
DependsOn []string
Annotations map[string]string
}
This model contains all necessary configuration:
- Container image, command, environment
- Volume mounts and configurations
- Network settings
- Service dependencies
- Build specifications
Key principle: Domain models ARE the platform-neutral representation. No separate intermediate format needed.
2. Compose Processing #
The compose processor parses Docker Compose files and converts them directly into platform-agnostic service.Spec models:
Docker Compose YAML → service.Spec models
The processor handles:
- Multi-container definitions
- Environment variable resolution
- Profile selection
- Compose-specific features (build, depends_on, etc.)
Important: The compose processor knows nothing about systemd, launchd, or any specific platform. It only produces domain models.
3. Platform Adapters #
Platform adapters implement two key interfaces:
Renderer Interface #
Converts domain models to platform-specific artifacts:
type Renderer interface {
Name() string
Render(ctx context.Context, specs []service.Spec) ([]Artifact, error)
}
systemd: Renders service.Spec → .container, .volume, .network Quadlet units
launchd: Renders service.Spec → .plist files (planned)
Lifecycle Interface #
Manages service operations on the target platform:
type Lifecycle interface {
Name() string
Reload(ctx context.Context) error
Start(ctx context.Context, name string) error
Stop(ctx context.Context, name string) error
Restart(ctx context.Context, name string) error
Status(ctx context.Context, name string) (ServiceStatus, error)
}
systemd: Uses D-Bus API for service management
launchd: Uses launchctl commands (planned)
4. Command Orchestration #
Commands orchestrate components directly through dependency injection. For example, the sync command:
- Syncs git repositories (GitSyncer)
- Processes changed Compose files (ComposeProcessor →
[]service.Spec) - Renders specs to platform artifacts (Renderer)
- Writes artifacts to disk (ArtifactStore)
- Reloads service manager (Lifecycle)
- Restarts changed services (Lifecycle)
Data Flow #
Sync Workflow #
sequenceDiagram
participant User
participant Sync Command
participant GitSyncer
participant ComposeProcessor
participant Renderer
participant ArtifactStore
participant Lifecycle
User->>Sync Command: quad-ops sync
Sync Command->>GitSyncer: Sync repositories
GitSyncer-->>Sync Command: Changed files
loop For each changed repo
Sync Command->>ComposeProcessor: Process compose files
ComposeProcessor-->>Sync Command: []service.Spec models
Sync Command->>Renderer: Render specs
Note over Renderer: Platform-specific<br/>(systemd/launchd)
Renderer-->>Sync Command: Platform artifacts
Sync Command->>ArtifactStore: Write artifacts
Sync Command->>Lifecycle: Reload daemon
Sync Command->>Lifecycle: Restart services
end
Platform Selection #
Platform detection happens at startup in cmd/root.go:
func NewApp() (*App, error) {
var renderer platform.Renderer
var lifecycle platform.Lifecycle
switch runtime.GOOS {
case "linux":
renderer = systemd.NewRenderer(...)
lifecycle = systemd.NewLifecycle(...)
case "darwin":
renderer = launchd.NewRenderer(...)
lifecycle = launchd.NewLifecycle(...)
default:
return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
return &App{
PlatformRenderer: renderer,
PlatformLifecycle: lifecycle,
// ... other components
}, nil
}
Platform Implementations #
Linux (systemd + Quadlet) #
Current implementation - Fully supported
The systemd adapter leverages Podman Quadlet for container management:
Renderer: Converts
service.Specto Quadlet unit files.containerfiles for services.volumefiles for named volumes.networkfiles for custom networks.buildfiles for image builds
Lifecycle: Uses D-Bus API for systemd operations
systemctl daemon-reloadafter changes- Service start/stop/restart
- Status queries and monitoring
Benefits:
- Native systemd journal integration
- Automatic dependency resolution
- systemd’s robust restart policies
- Socket activation support
macOS (launchd) #
Planned implementation
The launchd adapter will provide similar functionality for macOS:
Renderer: Converts
service.Specto launchd.plistfiles- Service definition in property list format
- Environment configuration
- Dependency management
Lifecycle: Uses
launchctlcommands- Load/unload services
- Start/stop operations
- Status monitoring
Container Runtime: Works with Docker Desktop or Podman
- Configurable container runtime
- Same container definitions across platforms
Adding New Platforms #
The architecture makes it straightforward to add new platforms:
- Create
internal/platform/{platform}/directory - Implement
Rendererinterface (convertservice.Spec→ platform artifacts) - Implement
Lifecycleinterface (manage services on that platform) - Add platform detection in
cmd/root.go
No changes needed in:
- Compose processing
- Service models
- Repository sync
- Command implementations
This separation ensures quad-ops can expand to support Windows Services, Kubernetes, or other platforms without touching core logic.
Design Benefits #
- Cross-Platform: Easy to support new service managers
- Testable: Clear interfaces enable comprehensive testing
- Maintainable: Platform logic isolated in adapters
- Simple: Domain models eliminate unnecessary abstraction layers
- Extensible: New platforms don’t affect existing code
- DI-First: No global state, fully injectable dependencies
- Standards-Based: Docker Compose as universal input format
Package Organization #
quad-ops/
├── cmd/ # CLI + orchestration
│ ├── root.go # App DI container + platform selection
│ ├── sync.go # Sync command
│ ├── daemon.go # Daemon command
│ └── ...
│
├── internal/
│ ├── compose/ # Docker Compose → service.Spec
│ │ └── processor.go
│ │
│ ├── service/ # Platform-agnostic models
│ │ ├── models.go # Spec, Container, Volume, Network
│ │ └── validate.go
│ │
│ ├── platform/ # Platform abstraction
│ │ ├── interfaces.go # Renderer, Lifecycle
│ │ │
│ │ ├── systemd/ # Linux implementation
│ │ │ ├── renderer.go # service.Spec → Quadlet units
│ │ │ └── lifecycle.go # systemd D-Bus operations
│ │ │
│ │ └── launchd/ # macOS implementation (planned)
│ │ ├── renderer.go # service.Spec → .plist files
│ │ └── lifecycle.go # launchctl operations
│ │
│ ├── repository/ # Git sync + artifact storage
│ └── config/ # Configuration management
Philosophy #
“Make it as simple as possible, but no simpler”
- Domain models can be platform-agnostic without a separate IR layer
- Commands can orchestrate directly without an app/orchestrator layer
- Platform adapters handle platform-specific rendering
- Less abstraction = easier to understand and maintain
- Pragmatic over theoretical purity