Behaviour
Reconciliation Model¶
ConfigBundleOperator follows a reconciliation-based control loop.
A ConfigBundle represents the desired configuration, while the corresponding
ConfigMap represents the managed state. The operator continuously evaluates
the relationship between these resources and takes action when the observed
state does not match the desired state.
flowchart LR
A["ConfigBundle<br/>Desired State"]
B["Operator"]
C["ConfigMap<br/>Managed State"]
A -->|"observe"| B
C -->|"observe"| B
B -->|"reconcile"| C
The reconciliation process can be simplified into three steps:
flowchart LR
A["Observe"]
B["Evaluate"]
C["Act"]
A --> B
B --> C
C --> A
Observe¶
The operator observes the current ConfigBundle and the state of its managed
ConfigMap.
Evaluate¶
The observed state is evaluated against the desired configuration defined by
the ConfigBundle.
flowchart LR
A["Desired State"]
B["Actual State"]
A --> C{"Match?"}
B --> C
C -->|"Yes"| D["No Action"]
C -->|"No"| E["Reconcile"]
Act¶
When the states do not match, the operator applies the required change to the
managed ConfigMap.
After the action is completed, the operator returns to the observation phase. This makes reconciliation a continuous control loop rather than a one-time operation.
flowchart LR
A["Observe"] --> B["Evaluate"]
B --> C["Act"]
C --> A
D["Desired State"] -.-> B
E["Actual State"] -.-> B
The specific behavior for creation, updates, drift, deletion, immutability, errors, and status conditions is described in the following sections.
Initial Reconciliation¶
When a ConfigBundle is created, ConfigBundleOperator handles the creation
event through the create_fn handler.
The handler converts the resource specification into a ConfigBundleSpec,
constructs the managed ConfigMap, and creates it through the Kubernetes API.
flowchart TB
A["ConfigBundle<br/>Create Event"]
B["create_fn"]
C["ConfigBundleSpec"]
D["ConfigMap"]
E["Kubernetes API"]
F["Update Status"]
A --> B
B -->|"parse spec"| C
C -->|"build"| D
D -->|"create"| E
E -->|"success"| F
Build the ConfigMap¶
The message field is stored as the message entry in the ConfigMap's
data, while the immutable field is passed directly to the ConfigMap.
flowchart TB
A["ConfigBundle.spec"]
B["message"]
C["immutable"]
D["ConfigMap"]
A --> B
A --> C
B -->|"data.message"| D
C -->|"immutable"| D
Create the ConfigMap¶
The operator then sends the ConfigMap to the Kubernetes API using the
AsyncClient.
flowchart LR
A["ConfigMap"]
B["AsyncClient"]
C["Kubernetes API"]
A -->|"create"| B
B --> C
If the API request fails with an ApiError, the handler raises a
kopf.PermanentError using the API error status.
flowchart LR
A["Create ConfigMap"]
B{"API request"}
C["ConfigMap Created"]
D["ApiError"]
E["PermanentError"]
A --> B
B -->|"success"| C
B -->|"failure"| D
D --> E
Update Status¶
After the ConfigMap is successfully created, the handler generates the
ConfigBundle status from the created ConfigMap and patches the resource's
status.
flowchart LR
A["Created ConfigMap"]
B["get_configbundle_status()"]
C["ConfigBundle.status"]
A --> B
B -->|"patch"| C
The complete creation flow is therefore:
flowchart TB
A["ConfigBundle<br/>Create"]
B["create_fn"]
C["ConfigBundleSpec"]
D["ConfigMap"]
E["adopt"]
F["AsyncClient.create"]
G{"Result"}
H["PermanentError"]
I["get_configbundle_status"]
J["Patch status"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G -->|"success"| I
G -->|"ApiError"| H
I --> J
This handler is responsible for the initial creation path. The behavior of existing ConfigMaps, subsequent updates, drift, and other reconciliation scenarios is covered in the following sections.
Update Behaviour¶
ConfigBundleOperator handles updates to a ConfigBundle through two separate
paths:
- Changes to
spec - Changes to
metadata.labelsormetadata.annotations
The behavior depends on which part of the resource has changed.
flowchart TD
A["ConfigBundle Updated"]
B{"What changed?"}
C["spec"]
D["metadata.labels"]
E["metadata.annotations"]
A --> B
B --> C
B --> D
B --> E
C --> F["Update Spec Behaviour"]
D --> G["Update ConfigMap Metadata"]
E --> G
Spec Updates¶
When the spec changes, the operator first evaluates the previous
ConfigBundleSpec.
flowchart TD
A["spec changed"]
B["Read old spec"]
C{"old_spec.immutable?"}
A --> B
B --> C
C -->|"Yes"| D["Restore previous spec"]
C -->|"No"| E["Build new ConfigMap state"]
E --> F["Patch ConfigMap"]
F --> G["Update status"]
The previous specification is used to determine whether the resource is immutable.
Immutable ConfigBundle¶
If the previous spec.immutable value is true, the update is rejected by
restoring the previous message and immutable values on the ConfigBundle
itself.
The managed ConfigMap is not modified.
flowchart TB
A["spec update"]
B["Previous spec"]
C{"immutable = true?"}
D["Patch ConfigBundle"]
E["Restore previous spec"]
F["No ConfigMap update"]
A --> B
B --> C
C -->|"Yes"| D
D --> E
E --> F
This means that once a ConfigBundle has been made immutable, subsequent
changes to its spec are reverted to the previous value.
Mutable ConfigBundle¶
If the previous spec.immutable value is false, the new specification is
converted into the corresponding ConfigMap state.
The operator updates both the ConfigMap data and its immutable property.
The ConfigMap is identified using the metadata stored in
ConfigBundle.status.
flowchart LR
A["New ConfigBundle.spec"]
B["ConfigMap patch"]
C["Managed ConfigMap"]
D["Updated status"]
A --> B
B --> C
C --> D
If the ConfigMap update succeeds, the resulting ConfigMap is used to generate
the new ConfigBundle.status.
If the Kubernetes API returns an error while updating the ConfigMap, the operator raises a temporary error and Kopf can retry the operation.
flowchart LR
A["Patch ConfigMap"]
B{"API result"}
C["Update status"]
D["TemporaryError"]
E["Retry"]
A --> B
B -->|"Success"| C
B -->|"ApiError"| D
D --> E
Metadata Updates¶
Changes to metadata.labels and metadata.annotations are handled
independently from spec changes.
When either field changes, the operator calculates the corresponding metadata change and applies it to the managed ConfigMap.
flowchart LR
A["ConfigBundle metadata"]
B{"Changed field"}
C["labels"]
D["annotations"]
E["Managed ConfigMap"]
A --> B
B --> C
B --> D
C --> E
D --> E
The operator handles three metadata operations:
- Adding labels or annotations
- Updating existing labels or annotations
- Removing labels or annotations
flowchart TD
A["Metadata change"]
B{"Operation"}
C["Add"]
D["Change"]
E["Remove"]
F["Update ConfigMap metadata"]
A --> B
B --> C
B --> D
B --> E
C --> F
D --> F
E --> F
The update is applied directly to the managed ConfigMap using the corresponding metadata field.
For example, adding a label to the ConfigBundle results in the same label
being added to its managed ConfigMap.
The same behavior applies to annotations.
Update Summary¶
The complete update behavior can be summarized as follows:
flowchart TD
A["ConfigBundle Update"]
B{"Changed field"}
A --> B
B -->|"spec"| C{"Previous immutable?"}
C -->|"Yes"| D["Restore previous spec"]
C -->|"No"| E["Patch ConfigMap"]
E --> F["Update status"]
B -->|"labels"| G["Update ConfigMap labels"]
B -->|"annotations"| H["Update ConfigMap annotations"]
Spec changes therefore affect the configuration of the managed ConfigMap, while label and annotation changes affect only its metadata.
Drift Detection & Self-Healing¶
ConfigBundleOperator continuously monitors the managed ConfigMap through a
Kopf daemon.
The daemon periodically compares the actual ConfigMap in the cluster with
the desired configuration derived from the ConfigBundle.
flowchart LR
A["ConfigBundle"]
B["Watch Loop"]
C["Desired ConfigMap"]
D["Actual ConfigMap"]
E{"Integrity Match?"}
A --> B
B --> C
B --> D
C --> E
D --> E
E -->|"Yes"| F["Wait"]
E -->|"No"| G["Heal"]
G --> F
F --> B
Watch Loop¶
The self-healing logic runs as a Kopf daemon associated with the
ConfigBundle resource.
The daemon repeatedly performs the following operations:
- Builds the desired
ConfigMapfrom the currentConfigBundle.spec. - Retrieves the managed
ConfigMapusing the metadata stored inConfigBundle.status. - Checks whether the actual ConfigMap matches the desired configuration.
- Heals the ConfigMap when it is missing or its state has drifted.
- Waits for the configured watch-loop interval before checking again.
The loop continues until the daemon is cancelled.
Integrity Check¶
The operator does not compare the entire Kubernetes object blindly.
The integrity check compares the following properties:
metadata.labelsmetadata.namespacemetadata.ownerReferencesdataimmutable
The comparison can be represented as:
flowchart TD
A["Actual ConfigMap"]
B["Desired ConfigMap"]
A --> C["Compare"]
B --> C
C --> D{"Equal?"}
D -->|"Yes"| E["No Action"]
D -->|"No"| F["Replace ConfigMap"]
If all compared properties are equal, the integrity check returns without modifying the ConfigMap.
If any of them differs, the desired configuration is applied to the existing
ConfigMap using a Kubernetes replace operation.
Before replacement, the name of the desired ConfigMap is set to the name of the actual ConfigMap. This preserves the existing managed resource identity while replacing its contents and relevant metadata.
ConfigMap Drift¶
A drift occurs when the actual ConfigMap differs from the state derived from
the ConfigBundle.
For example, if the message stored in the ConfigMap is changed manually,
the next integrity check detects that the data no longer matches the desired
configuration.
flowchart TB
A["ConfigBundle.spec"]
B["Desired ConfigMap"]
C["Actual ConfigMap"]
A --> B
B --> D{"Integrity Check"}
C --> D
D -->|"Drift detected"| E["Replace"]
E --> C
The replacement restores the ConfigMap to the desired state.
The same mechanism applies to differences in labels, namespace,
owner references, and the immutable property.
Missing ConfigMap¶
The daemon also handles the case where the managed ConfigMap no longer exists.
When the API returns a 404, the operator creates the ConfigMap again using
the desired configuration.
The flow is:
flowchart TD
A["Get Managed ConfigMap"]
B{"Exists?"}
A --> B
B -->|"Yes"| C["Check Integrity"]
B -->|"No - 404"| D["Create ConfigMap"]
D --> E["Update Status"]
E --> F["Temporary Recheck"]
The newly created ConfigMap is then used to update the ConfigBundle.status.
If creation fails with an ApiError, the operator raises a temporary error
with a three-second delay.
Successful Healing¶
Both drift correction and recovery from a missing ConfigMap update the
ConfigBundle.status using the resulting ConfigMap.
The complete self-healing flow is therefore:
flowchart TD
A["Watch Loop"]
B["Build Desired ConfigMap"]
C["Get Actual ConfigMap"]
D{"ConfigMap exists?"}
A --> B
B --> C
C --> D
D -->|"No - 404"| E["Create ConfigMap"]
E --> H["Update Status"]
D -->|"Yes"| F["Check Integrity"]
F --> G{"Matches Desired State?"}
G -->|"Yes"| I["No Action"]
G -->|"No"| J["Replace ConfigMap"]
J --> H
H --> K["Temporary Recheck"]
I --> L["Wait"]
K --> L
L --> A
The watch loop therefore provides two forms of self-healing:
- Missing resource recovery: recreate the ConfigMap when it no longer exists.
- Drift correction: replace the ConfigMap when its relevant state differs from the desired configuration.
The daemon waits for settings.configbundle_watch_loop_wait between checks.
The daemon's cancellation timeout is configured as 2 seconds.
Deletion Behaviour¶
The managed ConfigMap is associated with its ConfigBundle through a
Kubernetes ownerReference.
This relationship allows Kubernetes to manage the lifecycle of the
ConfigMap when its owning ConfigBundle is deleted.
ConfigBundle Deletion¶
When a ConfigBundle is deleted, its managed ConfigMap has an
ownerReference pointing to the ConfigBundle.
The relationship is:
flowchart LR
A["ConfigBundle<br/>Owner"]
B["ConfigMap<br/>Dependent"]
A -->|"ownerReference"| B
Because the ConfigMap is a dependent resource of the ConfigBundle,
Kubernetes garbage collection can remove the ConfigMap after its owner is
deleted.
The operator does not need to explicitly delete the ConfigMap as part of the
normal ConfigBundle deletion path.
flowchart LR
A["Delete ConfigBundle"]
B["ConfigBundle"]
C["ConfigMap"]
A --> B
B -->|"ownerReference"| C
B -. "deleted" .-> D["Garbage Collection"]
D -->|"delete dependent"| C
This keeps ownership and lifecycle management within Kubernetes itself rather than requiring a separate cleanup handler in the operator.
ConfigMap Deletion¶
The opposite case is different.
Deleting the managed ConfigMap does not delete the ConfigBundle, because the
ownership relationship is one-way. The ConfigBundle remains the source of the desired configuration.
If the ConfigMap is deleted while its ConfigBundle still exists, the
operator's watch loop detects that the managed ConfigMap is missing and
recreates it from the current ConfigBundle.spec.
flowchart LR
A["ConfigBundle"]
B["ConfigMap"]
A -->|"ownerReference"| B
B -. "deleted" .-> C["Missing ConfigMap"]
A --> D["Watch Loop"]
C --> D
D -->|"recreate"| B
This means the two deletion scenarios have different outcomes:
| Event | Result |
|---|---|
ConfigBundle is deleted |
Its managed ConfigMap becomes eligible for Kubernetes garbage collection |
Managed ConfigMap is deleted |
ConfigBundle remains and the operator recreates the ConfigMap |
The ownerReference therefore handles the downward lifecycle from
ConfigBundle to ConfigMap, while the operator's self-healing mechanism
handles the opposite case where the managed ConfigMap disappears.
Error Handling¶
ConfigBundleOperator distinguishes between errors that should stop an operation and errors that can be retried.
The error handling strategy depends on where the error occurs and whether the operation is considered recoverable.
flowchart TD
A["Kubernetes API Operation"]
B{"API Error"}
A --> B
B -->|"Create"| C["PermanentError"]
B -->|"Update"| D["TemporaryError"]
B -->|"Watch / Read"| E["TemporaryError"]
C --> F["Stop Operation"]
D --> G["Retry"]
E --> G
Create Errors¶
During the initial creation of the managed ConfigMap, an ApiError is
converted into a kopf.PermanentError.
This indicates that the creation operation is not retried by the handler as a temporary failure.
The flow is:
flowchart LR
A["Create ConfigMap"]
B{"API Result"}
A --> B
B -->|"Success"| C["Update Status"]
B -->|"ApiError"| D["PermanentError"]
Update Errors¶
When updating the managed ConfigMap as a result of a spec change, an
ApiError is converted into a kopf.TemporaryError.
This allows Kopf to retry the operation.
flowchart LR
A["Patch ConfigMap"]
B{"API Result"}
A --> B
B -->|"Success"| C["Update Status"]
B -->|"ApiError"| D["TemporaryError"]
D --> E["Retry"]
Missing ConfigMap¶
A missing managed ConfigMap is treated differently from a general API
failure.
When the watch loop receives a 404, the operator interprets it as a missing
managed resource and attempts to recreate it.
flowchart LR
A["Get ConfigMap"]
B["404 Not Found"]
C["Create ConfigMap"]
D["Update Status"]
A --> B
B --> C
C --> D
If recreation fails, the operator raises a TemporaryError with a three-second
delay.
Watch Loop Errors¶
For API errors encountered while reading the managed ConfigMap, errors other
than 404 are treated as temporary failures.
The operator raises a TemporaryError with a three-second delay, allowing the
watch loop to retry the operation.
flowchart LR
A["Get ConfigMap"]
B{"API Result"}
A --> B
B -->|"404"| C["Recreate ConfigMap"]
B -->|"Other ApiError"| D["TemporaryError"]
D -->|"delay = 3s"| E["Retry"]
B -->|"Success"| F["Integrity Check"]
Error Handling Summary¶
The current error handling behavior can be summarized as follows:
| Operation | Error | Handling | Retry |
|---|---|---|---|
| Create ConfigMap | ApiError |
PermanentError |
No |
| Update ConfigMap | ApiError |
TemporaryError |
Yes |
| Get ConfigMap | 404 |
Recreate ConfigMap | Yes |
| Get ConfigMap | Other ApiError |
TemporaryError |
Yes |
| Recreate ConfigMap | ApiError |
TemporaryError |
Yes |
The distinction between PermanentError and TemporaryError is intentional:
creation errors are treated as permanent by the create handler, while errors
encountered during updates and the daemon's watch loop are treated as
temporary and can be retried.