Add identity resource requires handler
Implement Identity resource requires handler. Depends-On: https://review.opendev.org/c/openstack/charm-keystone-k8s/+/891651 Change-Id: I0b320e37447e368619225c4c4832483bb476ec40
This commit is contained in:
parent
a4c869e0dc
commit
0b03d60129
@ -8,6 +8,7 @@ charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress
|
|||||||
charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
charmcraft fetch-lib charms.data_platform_libs.v0.database_requires
|
||||||
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
charmcraft fetch-lib charms.keystone_k8s.v1.identity_service
|
||||||
charmcraft fetch-lib charms.keystone_k8s.v0.identity_credentials
|
charmcraft fetch-lib charms.keystone_k8s.v0.identity_credentials
|
||||||
|
charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
|
||||||
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq
|
||||||
charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb
|
charmcraft fetch-lib charms.ovn_central_k8s.v0.ovsdb
|
||||||
charmcraft fetch-lib charms.traefik_k8s.v1.ingress
|
charmcraft fetch-lib charms.traefik_k8s.v1.ingress
|
||||||
|
@ -1058,3 +1058,77 @@ class IdentityCredentialsRequiresHandler(RelationHandler):
|
|||||||
return bool(self.interface.password)
|
return bool(self.interface.password)
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityResourceRequiresHandler(RelationHandler):
|
||||||
|
"""Handles the identity resource relation on the requires side."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
charm: ops.charm.CharmBase,
|
||||||
|
relation_name: str,
|
||||||
|
callback_f: Callable,
|
||||||
|
mandatory: bool = False,
|
||||||
|
):
|
||||||
|
"""Create a new identity-ops handler.
|
||||||
|
|
||||||
|
Create a new IdentityResourceRequiresHandler that handles initial
|
||||||
|
events from the relation and invokes the provided callbacks based on
|
||||||
|
the event raised.
|
||||||
|
|
||||||
|
:param charm: the Charm class the handler is for
|
||||||
|
:type charm: ops.charm.CharmBase
|
||||||
|
:param relation_name: the relation the handler is bound to
|
||||||
|
:type relation_name: str
|
||||||
|
:param callback_f: the function to call when the nodes are connected
|
||||||
|
:type callback_f: Callable
|
||||||
|
:param mandatory: If the relation is mandatory to proceed with
|
||||||
|
configuring charm
|
||||||
|
:type mandatory: bool
|
||||||
|
"""
|
||||||
|
super().__init__(charm, relation_name, callback_f, mandatory)
|
||||||
|
|
||||||
|
def setup_event_handler(self):
|
||||||
|
"""Configure event handlers for an Identity resource relation."""
|
||||||
|
import charms.keystone_k8s.v0.identity_resource as id_ops
|
||||||
|
|
||||||
|
logger.debug("Setting up Identity Resource event handler")
|
||||||
|
ops_svc = id_ops.IdentityResourceRequires(
|
||||||
|
self.charm,
|
||||||
|
self.relation_name,
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
ops_svc.on.provider_ready,
|
||||||
|
self._on_provider_ready,
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
ops_svc.on.provider_goneaway,
|
||||||
|
self._on_provider_goneaway,
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
ops_svc.on.response_available,
|
||||||
|
self._on_response_available,
|
||||||
|
)
|
||||||
|
return ops_svc
|
||||||
|
|
||||||
|
def _on_provider_ready(self, event) -> None:
|
||||||
|
"""Handles provider_ready event."""
|
||||||
|
logger.debug(
|
||||||
|
"Identity ops provider available and ready to process any requests"
|
||||||
|
)
|
||||||
|
self.callback_f(event)
|
||||||
|
|
||||||
|
def _on_provider_goneaway(self, event) -> None:
|
||||||
|
"""Handles provider_goneaway event."""
|
||||||
|
logger.info("Keystone provider not available process any requests")
|
||||||
|
self.callback_f(event)
|
||||||
|
|
||||||
|
def _on_response_available(self, event) -> None:
|
||||||
|
"""Handles response available events."""
|
||||||
|
logger.info("Handle response from identity ops")
|
||||||
|
self.callback_f(event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ready(self) -> bool:
|
||||||
|
"""Whether handler is ready for use."""
|
||||||
|
return self.interface.ready()
|
||||||
|
@ -0,0 +1,348 @@
|
|||||||
|
"""IdentityResourceProvides and Requires module.
|
||||||
|
|
||||||
|
|
||||||
|
This library contains the Requires and Provides classes for handling
|
||||||
|
the identity_ops interface.
|
||||||
|
|
||||||
|
Import `IdentityResourceRequires` in your charm, with the charm object and the
|
||||||
|
relation name:
|
||||||
|
- self
|
||||||
|
- "identity_ops"
|
||||||
|
|
||||||
|
Also provide additional parameters to the charm object:
|
||||||
|
- request
|
||||||
|
|
||||||
|
Three events are also available to respond to:
|
||||||
|
- provider_ready
|
||||||
|
- provider_goneaway
|
||||||
|
- response_avaialable
|
||||||
|
|
||||||
|
A basic example showing the usage of this relation follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
from charms.keystone_k8s.v0.identity_resource import IdentityResourceRequires
|
||||||
|
|
||||||
|
class IdentityResourceClientCharm(CharmBase):
|
||||||
|
def __init__(self, *args):
|
||||||
|
super().__init__(*args)
|
||||||
|
# IdentityResource Requires
|
||||||
|
self.identity_resource = IdentityResourceRequires(
|
||||||
|
self, "identity_ops",
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
self.identity_resource.on.provider_ready, self._on_identity_resource_ready)
|
||||||
|
self.framework.observe(
|
||||||
|
self.identity_resource.on.provider_goneaway, self._on_identity_resource_goneaway)
|
||||||
|
self.framework.observe(
|
||||||
|
self.identity_resource.on.response_available, self._on_identity_resource_response)
|
||||||
|
|
||||||
|
def _on_identity_resource_ready(self, event):
|
||||||
|
'''React to the IdentityResource provider_ready event.
|
||||||
|
|
||||||
|
This event happens when n IdentityResource relation is added to the
|
||||||
|
model. Ready to send any ops to keystone.
|
||||||
|
'''
|
||||||
|
# Ready to send any ops.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_identity_resource_response(self, event):
|
||||||
|
'''React to the IdentityResource response_available event.
|
||||||
|
|
||||||
|
The IdentityResource interface will provide the response for the ops sent.
|
||||||
|
'''
|
||||||
|
# Read the response for the ops sent.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _on_identity_resource_goneaway(self, event):
|
||||||
|
'''React to the IdentityResource goneaway event.
|
||||||
|
|
||||||
|
This event happens when an IdentityResource relation is removed.
|
||||||
|
'''
|
||||||
|
# IdentityResource Relation has goneaway. No ops can be sent.
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ops.framework import (
|
||||||
|
EventBase,
|
||||||
|
EventSource,
|
||||||
|
Object,
|
||||||
|
ObjectEvents,
|
||||||
|
StoredState,
|
||||||
|
)
|
||||||
|
from ops.model import (
|
||||||
|
Relation,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# The unique Charmhub library identifier, never change it
|
||||||
|
LIBID = "b419d4d8249e423487daafc3665ed06f"
|
||||||
|
|
||||||
|
# Increment this major API version when introducing breaking changes
|
||||||
|
LIBAPI = 0
|
||||||
|
|
||||||
|
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||||
|
# to 0 if you are raising the major API version
|
||||||
|
LIBPATCH = 1
|
||||||
|
|
||||||
|
|
||||||
|
REQUEST_NOT_SENT = 1
|
||||||
|
REQUEST_SENT = 2
|
||||||
|
REQUEST_PROCESSED = 3
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityOpsProviderReadyEvent(EventBase):
|
||||||
|
"""Has IdentityOpsProviderReady Event."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityOpsResponseEvent(EventBase):
|
||||||
|
"""Has IdentityOpsResponse Event."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityOpsProviderGoneAwayEvent(EventBase):
|
||||||
|
"""Has IdentityOpsProviderGoneAway Event."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityResourceResponseEvents(ObjectEvents):
|
||||||
|
"""Events class for `on`."""
|
||||||
|
|
||||||
|
provider_ready = EventSource(IdentityOpsProviderReadyEvent)
|
||||||
|
response_available = EventSource(IdentityOpsResponseEvent)
|
||||||
|
provider_goneaway = EventSource(IdentityOpsProviderGoneAwayEvent)
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityResourceRequires(Object):
|
||||||
|
"""IdentityResourceRequires class."""
|
||||||
|
|
||||||
|
on = IdentityResourceResponseEvents()
|
||||||
|
_stored = StoredState()
|
||||||
|
|
||||||
|
def __init__(self, charm, relation_name):
|
||||||
|
super().__init__(charm, relation_name)
|
||||||
|
self.charm = charm
|
||||||
|
self.relation_name = relation_name
|
||||||
|
self._stored.set_default(provider_ready=False, requests=[])
|
||||||
|
self.framework.observe(
|
||||||
|
self.charm.on[relation_name].relation_joined,
|
||||||
|
self._on_identity_resource_relation_joined,
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
self.charm.on[relation_name].relation_changed,
|
||||||
|
self._on_identity_resource_relation_changed,
|
||||||
|
)
|
||||||
|
self.framework.observe(
|
||||||
|
self.charm.on[relation_name].relation_broken,
|
||||||
|
self._on_identity_resource_relation_broken,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_identity_resource_relation_joined(self, event):
|
||||||
|
"""Handle IdentityResource joined."""
|
||||||
|
self._stored.provider_ready = True
|
||||||
|
self.on.provider_ready.emit()
|
||||||
|
|
||||||
|
def _on_identity_resource_relation_changed(self, event):
|
||||||
|
"""Handle IdentityResource changed."""
|
||||||
|
id_ = self.response.get("id")
|
||||||
|
self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
|
||||||
|
self.on.response_available.emit()
|
||||||
|
|
||||||
|
def _on_identity_resource_relation_broken(self, event):
|
||||||
|
"""Handle IdentityResource broken."""
|
||||||
|
self._stored.provider_ready = False
|
||||||
|
self.on.provider_goneaway.emit()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _identity_resource_rel(self) -> Relation:
|
||||||
|
"""The IdentityResource relation."""
|
||||||
|
return self.framework.model.get_relation(self.relation_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def response(self) -> dict:
|
||||||
|
"""Response object from keystone."""
|
||||||
|
response = self.get_remote_app_data("response")
|
||||||
|
if not response:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(str(e))
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_request_in_store(self, id: str, tag: str, ops: list, state: int):
|
||||||
|
"""Save request in the store."""
|
||||||
|
if id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
for request in self._stored.requests:
|
||||||
|
if request.get("id") == id:
|
||||||
|
if tag:
|
||||||
|
request["tag"] = tag
|
||||||
|
if ops:
|
||||||
|
request["ops"] = ops
|
||||||
|
request["state"] = state
|
||||||
|
return
|
||||||
|
|
||||||
|
# New request
|
||||||
|
self._stored.requests.append(
|
||||||
|
{"id": id, "tag": tag, "ops": ops, "state": state}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_request_from_store(self, id: str) -> dict:
|
||||||
|
"""Get request from the stote."""
|
||||||
|
for request in self._stored.requests:
|
||||||
|
if request.get("id") == id:
|
||||||
|
return request
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def is_request_processed(self, id: str) -> bool:
|
||||||
|
"""Check if request is processed."""
|
||||||
|
for request in self._stored.requests:
|
||||||
|
if (
|
||||||
|
request.get("id") == id
|
||||||
|
and request.get("state") == REQUEST_PROCESSED
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_remote_app_data(self, key: str) -> str:
|
||||||
|
"""Return the value for the given key from remote app data."""
|
||||||
|
data = self._identity_resource_rel.data[
|
||||||
|
self._identity_resource_rel.app
|
||||||
|
]
|
||||||
|
return data.get(key)
|
||||||
|
|
||||||
|
def ready(self) -> bool:
|
||||||
|
"""Interface is ready or not.
|
||||||
|
|
||||||
|
Interface is considered ready if the op request is processed
|
||||||
|
and response is sent. In case of non leader unit, just consider
|
||||||
|
the interface is ready.
|
||||||
|
"""
|
||||||
|
if not self.model.unit.is_leader():
|
||||||
|
logger.debug("Not a leader unit, set the interface to ready")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_data = self._identity_resource_rel.data[self.charm.app]
|
||||||
|
if "request" not in app_data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
request = json.loads(app_data["request"])
|
||||||
|
request_id = request.get("id")
|
||||||
|
response_id = self.response.get("id")
|
||||||
|
if request_id == response_id:
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(str(e))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def request_ops(self, request: dict) -> None:
|
||||||
|
"""Request keystone ops."""
|
||||||
|
if not self.model.unit.is_leader():
|
||||||
|
logger.debug("Not a leader unit, not sending request")
|
||||||
|
return
|
||||||
|
|
||||||
|
id_ = request.get("id")
|
||||||
|
tag = request.get("tag")
|
||||||
|
ops = request.get("ops")
|
||||||
|
req = self.get_request_from_store(id_)
|
||||||
|
if req and req.get("state") == REQUEST_PROCESSED:
|
||||||
|
logger.debug("Request {id_} already processed")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._stored.provider_ready:
|
||||||
|
self.save_request_in_store(id_, tag, ops, REQUEST_NOT_SENT)
|
||||||
|
logger.debug("Keystone not yet ready to take requests")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("Requesting ops to keystone")
|
||||||
|
app_data = self._identity_resource_rel.data[self.charm.app]
|
||||||
|
app_data["request"] = json.dumps(request)
|
||||||
|
self.save_request_in_store(id_, tag, ops, REQUEST_SENT)
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityOpsRequestEvent(EventBase):
|
||||||
|
"""Has IdentityOpsRequest Event."""
|
||||||
|
|
||||||
|
def __init__(self, handle, relation_id, relation_name, request):
|
||||||
|
"""Initialise event."""
|
||||||
|
super().__init__(handle)
|
||||||
|
self.relation_id = relation_id
|
||||||
|
self.relation_name = relation_name
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
def snapshot(self):
|
||||||
|
"""Snapshot the event."""
|
||||||
|
return {
|
||||||
|
"relation_id": self.relation_id,
|
||||||
|
"relation_name": self.relation_name,
|
||||||
|
"request": self.request,
|
||||||
|
}
|
||||||
|
|
||||||
|
def restore(self, snapshot):
|
||||||
|
"""Restore the event."""
|
||||||
|
super().restore(snapshot)
|
||||||
|
self.relation_id = snapshot["relation_id"]
|
||||||
|
self.relation_name = snapshot["relation_name"]
|
||||||
|
self.request = snapshot["request"]
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityResourceProviderEvents(ObjectEvents):
|
||||||
|
"""Events class for `on`."""
|
||||||
|
|
||||||
|
process_op = EventSource(IdentityOpsRequestEvent)
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityResourceProvides(Object):
|
||||||
|
"""IdentityResourceProvides class."""
|
||||||
|
|
||||||
|
on = IdentityResourceProviderEvents()
|
||||||
|
|
||||||
|
def __init__(self, charm, relation_name):
|
||||||
|
super().__init__(charm, relation_name)
|
||||||
|
self.charm = charm
|
||||||
|
self.relation_name = relation_name
|
||||||
|
self.framework.observe(
|
||||||
|
self.charm.on[relation_name].relation_changed,
|
||||||
|
self._on_identity_resource_relation_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_identity_resource_relation_changed(self, event):
|
||||||
|
"""Handle IdentityResource changed."""
|
||||||
|
request = event.relation.data[event.relation.app].get("request", {})
|
||||||
|
self.on.process_op.emit(
|
||||||
|
event.relation.id, event.relation.name, request
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_ops_response(
|
||||||
|
self, relation_id: str, relation_name: str, ops_response: dict
|
||||||
|
):
|
||||||
|
"""Set response to ops request."""
|
||||||
|
if not self.model.unit.is_leader():
|
||||||
|
logger.debug("Not a leader unit, not sending response")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("Update response from keystone")
|
||||||
|
_identity_resource_rel = self.charm.model.get_relation(relation_name, relation_id)
|
||||||
|
if not _identity_resource_rel:
|
||||||
|
# Relation has disappeared so skip send of data
|
||||||
|
return
|
||||||
|
|
||||||
|
app_data = _identity_resource_rel.data[self.charm.app]
|
||||||
|
app_data["response"] = json.dumps(ops_response)
|
Loading…
Reference in New Issue
Block a user