Shell-operator package for OpenStack reconciliation hooks.
Each hook reconciles one Kubernetes CRD into one kind of OpenStack resource. The
generic machinery lives in openstack_sync/hooks/framework.py; a plugin supplies
only the parts that are specific to its resource.
openstack_sync/
utils.py Kubernetes Secret access + memoised connections
hooks/
common.py binding-context I/O, CR status patching
framework.py HookConfig, SyncPlugin, run_sync(), run_hook()
placeholder.py connectivity probe (no CRs)
<resource>.py CRD hook entry point
plugins/
common.py OpenStack helpers shared by all plugins
<service>/<resource>/
config.py plugin constants
client.py OpenStack API calls, if needed
markers.py ownership markers
reconcile.py converge one CR
prune.py delete resources whose CR was removed, if safe
run_sync groups CRs by the credentials in spec.cloudCredentialsRef, opens one
connection per credential group, waits for the OpenStack service, reconciles each
CR, patches Synced/Failed onto the CR status, and then calls the plugin's
prune step, which most plugins gate on PRUNE. If any reconcile fails, or any CR
could not be read at all, it skips the prune entirely - either way the
desired state is unknown, so deleting anything would be unsafe.
A CR whose spec does not satisfy the framework's contract is named in the log and dropped, and the run exits non-zero. The remaining CRs still reconcile: one unusable object must not stall a whole namespace.
run_hook handles the shell-operator calling convention: --config, logging,
reading the binding context, and the exit code.
-
Write the CRD in
components/openstack-sync-operator/crds/. Include astatussubresource and a requiredspec.cloudCredentialsRefwithsecretNameandcloudName- the framework relies on both. Put validation (required,enum,minLength,default) in the schema so the API server rejects bad CRs at admission.The schema only validates writes, though. A CR stored before you tightened the schema is still served as stored, so give optional fields a default and fail one CR by name when a required field is missing.
-
Register it in
components/openstack-sync-operator/values.yaml:plugins: <resourceName>: false # opt in per site pluginData: <resourceName>: hook: path: /hooks/<resource>.py crd: crds/<group>_<plural>.yaml envPrefix: <ENV_PREFIX> env: SYNC_CRONTAB: "0 * * * *"
The chart derives
<ENV_PREFIX>_ENABLED,_CRD_API_VERSION,_CRD_KIND,_CRD_RESOURCEand_STATUS_ENABLEDfrom the CRD file, and turns eachenvkey into<ENV_PREFIX>_<KEY>.HookConfig.from_envreads only the framework keys, such asPRUNE,SYNC_CRONTAB,READY_RETRIESandREADY_DELAY. Plugins read custom prefixed env vars directly. -
Write the plugin package under
plugins/<service>/<resource>/.config.pyandreconcile.pyare the usual minimum. Addmarkers.pywhen the plugin stamps ownership into OpenStack resources, andprune.pyonly when deleting resources after CR removal is safe and implemented. -
Write the hook - subclass
SyncPluginand wire it up:class ResourcePlugin(SyncPlugin): noun = "<resource>" def wait_for_api(self, conn) -> None: ... def reconcile(self, conn, spec, cache) -> list[str]: return reconcile_module.sync(conn, spec, cache) def prune(self, conn, desired_specs, *, authoritative_empty) -> None: if self.config.prune: prune_module.prune(conn, desired_specs, authoritative_empty=authoritative_empty) def main() -> int: def run(contexts): if not hook_enabled(ENV_PREFIX): return 0 config = HookConfig.from_env(ENV_PREFIX, binding_name=BINDING_NAME) return run_sync(ResourcePlugin(config), hook_inputs(contexts, config)) return run_hook(lambda: build_crd_hook_config(ENV_PREFIX, BINDING_NAME), run)
wait_for_apiandreconcileare required;new_cacheandprunehave working defaults.
A CR is an ownership claim. Every plugin records ownership on the resources described by its CRs. If the matching OpenStack resource already exists, the plugin may stamp the ownership marker and reconcile it; after that, the resource is operator-managed and can be pruned when the CR is removed. Do not create a CR for a hand-made resource unless transferring it to the operator is intentional.
Report what you cannot fix. reconcile returns a list of notes. Use it for
state that diverges from the spec but that OpenStack will not let the operator
correct. The resource is still Synced, but the notes appear on the CR status
and in the logs so an operator can act. Raise an exception only for an actual
failure.
uvx ruff check openstack_sync/ tests/ 2>&1
uvx ruff format --check openstack_sync/ tests/ 2>&1
uv run pytesttests/test_framework.py exercises the driver with a stub plugin and no
OpenStack at all - read it first to understand the contract a plugin gets.