Skip to content

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.labels or metadata.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.

data:
  message: "<new message>"

immutable: <new immutable value>

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:

  1. Builds the desired ConfigMap from the current ConfigBundle.spec.
  2. Retrieves the managed ConfigMap using the metadata stored in ConfigBundle.status.
  3. Checks whether the actual ConfigMap matches the desired configuration.
  4. Heals the ConfigMap when it is missing or its state has drifted.
  5. 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.labels
  • metadata.namespace
  • metadata.ownerReferences
  • data
  • immutable

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.