Charm design overview¶
This page explains the design patterns and architecture used in both the Falco operator and Falcosidekick K8s operator charms.
Design philosophy¶
Both charms follow a consistent design pattern that separates concerns into distinct modules, making the codebase maintainable and testable. The design emphasizes:
Single source of truth: All configuration and relation data is aggregated into a single state object
Separation of concerns: Clear boundaries between charm logic, state management, and workload configuration
Declarative configuration: The workload is configured based on the desired state, not through imperative commands
Architecture pattern¶
Both charms implement the same architectural pattern:
graph LR
E[Events] --> H[Observed in charm.py]
H --> R[Relation Libraries]
H --> C[Configuration]
R --> S[State Module]
C --> S
S --> W[Workload/Service Module]
W --> T[Templates]
W --> WL[Workload/System]
style S fill:#ffe1e1
style W fill:#e1f5ff
style H fill:#fff4e1
The flow of data through the charm follows this pattern:
Events trigger handlers: Juju events (
config-changed,relation-changed) are observed incharm.pyData collection: Handlers gather data from configuration options and relation libraries
State aggregation: All data is combined into a single
CharmStateobject in thestate.pymoduleWorkload configuration: The workload module (
service.pyorworkload.py) receives the state and configures the service accordingly
Module responsibilities¶
charm.py¶
The main charm module coordinates the overall charm behavior:
Observes Juju lifecycle events (
install,config-changed,upgrade)Observes relation events (
relation-joined,relation-changed,relation-broken)Initializes relation libraries and helper objects
Delegates workload configuration to the service/workload module
For example, the Falco operator observes the http-endpoint and cos-agent relations and triggers reconciliation
when they change, while the Falcosidekick K8s operator observes multiple relations including
send-loki-logs, certificates, ingress, logging, grafana-dashboard, and metrics-endpoint.
state.py¶
The state module provides a single source of truth for all charm data:
Aggregates charm configuration options
Collects data from relation libraries
Validates and transforms data into a consistent format using Pydantic models
Provides a
CharmStateobject that represents the complete desired state
For example, in the Falcosidekick K8s operator, the state module combines:
Configuration options (port)
Loki endpoint from the
send-loki-logsrelationTLS certificate data from the
certificatesrelationIngress configuration from the
ingressrelation
All of this data is validated and packaged into a single CharmState object that the workload module can consume.
service.py / workload.py¶
The workload module configures the service based on the charm state:
Receives the
CharmStateobject from the charmRenders configuration templates with state data
Manages the lifecycle of the workload (install, configure, restart)
Interacts with the workload (systemd service for Falco, Pebble container for Falcosidekick K8s)
For the Falco operator, service.py manages:
Falco configuration files rendered from templates
Custom configuration from Git repositories
Systemd service lifecycle
For the Falcosidekick K8s operator, workload.py manages:
Pebble layer configuration
Container configuration files
TLS certificate installation
Health checks
Relation handlers¶
Both charms use relation libraries to handle integrations:
Falco operator: Uses
HttpEndpointRequirerto connect to Falcosidekick andCosAgentProviderfor metrics collectionFalcosidekick K8s operator: Uses
HttpEndpointProvider,LokiPushApiConsumer,LogForwarder,TlsCertificateRequirer,IngressPerAppRequirer,GrafanaDashboardProvider, andMetricsEndpointProvider
These libraries abstract the complexity of relation data exchange and provide clean interfaces for the charm to use.
Configuration flow¶
The configuration flow in both charms follows this sequence:
sequenceDiagram
participant J as Juju
participant C as charm.py
participant S as state.py
participant W as workload / service module
participant WL as Workload / Service
J->>C: Event (config-changed, relation-changed)
C->>C: Observe event
C->>S: CharmState.from_charm()
S->>S: Load config
S->>S: Gather relation data
S->>S: Validate and transform
S->>C: Return CharmState
C->>W: configure(state)
W->>W: Render templates
W->>WL: Apply configuration
W->>WL: Restart if needed
WL->>C: Service status
C->>J: Set unit status
This pattern ensures that:
Configuration is always derived from the current state
All changes go through the same validation and transformation logic
The workload is configured holistically rather than incrementally
Template system¶
Both charms use Jinja2 templates for configuration files. These templates are stored in the src/templates/ directory,
and the workload module renders templates with the charm state as context. The rendered files are installed in the
appropriate locations.
For example, the Falco operator uses templates for:
Falco configuration files
Systemd service files
The Falcosidekick K8s operator uses templates for:
Falcosidekick configuration files
Pebble layer definitions
Error handling¶
Both charms implement consistent error handling:
Configuration validation errors are caught and result in a
BlockedStatusMissing required relations result in a
BlockedStatuswith a clear messageRuntime errors are logged and reported through the unit status
This ensures that operators have clear visibility into charm state and can take corrective action when needed.