register new handlers during run (dynamically generating handlers) #967
Replies: 6 comments 2 replies
-
|
Hello. Can you please briefly describe the use-case behind this supposed solution? |
Beta Was this translation helpful? Give feedback.
-
|
The scenario I have in my head is to apply CRD with resource type specified in it, e.g. Services and to trigger some method, defined earlier. Then I would like to apply new CRD with specified another type of resource and call that method again. |
Beta Was this translation helpful? Give feedback.
-
|
Related: #378 |
Beta Was this translation helpful? Give feedback.
-
|
Currently, the operator is assumed to be statically formed. Dynamic registration/removal of handlers brings some complications to the API/DSL (not so much to the implementation). However, it might be implemented indefinitely later. Specifically, if handlers are dynamically registered, they must be dynamically viewable/iterable and dynamically removable. For that, a handler token must be returned and stored in memory by the operator for reference to the handler in the operator's internals (the handler itself can be a token though, as it is immutable). As of now, two ways are possible to implement this scenario: Way 1: watch for everything, react only to what is expected. import kopf
@kopf.on.startup()
def init(memo, **_):
memo.handled_resources = [] # list[tuple(group, plural), ...]
# For the resource that controls what to serve:
@kopf.on.create('metaresource')
@kopf.on.resume('metaresource')
def register(spec, memo, **_):
t = (spec['selector']['group'], spec['selector']['plural'])
memo.handled_resources.append(t)
@kopf.on.delete('metaresource')
def unregister(spec, memo, **_):
t = (spec['selector']['group'], spec['selector']['plural'])
if t in memo.handled_resources:
memo.handled_resources.remove(t)
# For regular resources being served:
def is_of_interest(resource, memo, **_):
t = (resource.group, resource.plural)
return t in memo.handled_resources
@kopf.on.event(kopf.EVERYTHING, when=is_of_interest)
def fn(resource, name, **_):
print(resource, name)The downside is that the operator will listen for everything (really everything). This adds extra load on the API servers (extra 10-20-30 connections — depends on how many resources you have). And pollutes logs with the watch-stream starting. Unrelated resources are not mentioned in the logs, they are filtered out silently — so it is not a big problem, just some hassle. Way 2: Register specialised handlers in the meta-resource creation. import kopf
@kopf.on.create('metaresource')
@kopf.on.resume('metaresource')
def register(spec, memo, **_):
@kopf.on.event(spec['selector']['group'], spec['selector']['plural'])
def fn(resource, name, **_):
print(resource, name)The upside: It only listens/watches for what was registered at least once, not for everything. The downside 1: beware of the closures. Here, spec & memo are of the meta-resource, not of the served resource. You need to add spec & memo to fn() to get the proper values. The downside 2: there is no de-registration (removal). So, the handlers remain forever. When the meta-resource is removed, the fn() remains there. Some memory leaks are possible over time (when meta-resources are added/removed often). Double-registration is also possible (when meta-resource are removed and then re-added for the same target). To partially mitigate that, a bool-container can be used to toggle handlers off later (e.g. an Event, but anything would work): import kopf
import threading
@kopf.on.create('metaresource')
@kopf.on.resume('metaresource')
def register(spec, memo, **_):
memo.disabled = disabled = threading.Event() # initially False
@kopf.on.event(spec['selector']['group'], spec['selector']['plural'], when=lambda **_: not disabled.is_set())
def fn(resource, name, **_):
print(resource, name)
@kopf.on.delete('metaresource')
def unregister(spec, memo, **_):
memo.disabled.set() # deactivate the fn-handler (but it stays in memory)Still, the watch-streams of un-registered resources will remain there — even if the handlers are disabled. Restarting the operator from time to time (e.g. daily) can help with cleanups. Both ways are hacky. I will think about a nicer way with an extra API of the framework. |
Beta Was this translation helpful? Give feedback.
-
|
@nolar Any updates on this issue since 2021? |
Beta Was this translation helpful? Give feedback.
-
Is it still expected to work? I'm trying to implement it with kopf==1.36.1: import kopf
def manifest_created(logger, **_):
logger.info("A new manifest is created")
@kopf.on.resume('', 'v1', 'ConfigMap')
@kopf.on.create('', 'v1', 'ConfigMap')
def on_create_clusterobject(spec, namespace, logger, **_):
kopf.on.create('', 'v1', 'Secret')(manifest_created)
logger.info(kopf.get_default_registry()._changing.__dict__)and I see the handler in the registry: but a Secret creation event does not trigger it (no logs generated by |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Question
Is there a possibility to register new handlers while kopf is already running, like creating dynamically handlers ? I only want to specify new resources as the handler method will be always the same.
Checklist
Keywords
dynamic handlers
Beta Was this translation helpful? Give feedback.
All reactions