Keystone K8S
- OpenStack Charmers
Channel | Revision | Published | Runs on |
---|---|---|---|
yoga/beta | 82 | 10 Mar 2023 | |
yoga/edge | 99 | 16 May 2023 | |
xena/beta | 72 | 21 Nov 2022 | |
xena/edge | 76 | 20 Jan 2023 | |
2024.1/beta | 214 | 07 Jan 2025 | |
2024.1/beta | 208 | 22 Nov 2024 | |
2024.1/edge | 214 | 13 Dec 2024 | |
2024.1/edge | 208 | 07 Oct 2024 | |
2023.2/stable | 148 | 06 Dec 2023 | |
2023.2/candidate | 169 | 05 Apr 2024 | |
2023.2/beta | 169 | 05 Apr 2024 | |
2023.2/edge | 169 | 03 Apr 2024 | |
2023.1/stable | 125 | 26 Sep 2023 | |
2023.1/candidate | 155 | 12 Jan 2024 | |
2023.1/beta | 155 | 10 Jan 2024 | |
2023.1/edge | 155 | 03 Jan 2024 |
juju deploy keystone-k8s --channel 2023.2/stable
Deploy Kubernetes operators easily with Juju, the Universal Operator Lifecycle Manager. Need a Kubernetes cluster? Install MicroK8s to create a full CNCF-certified Kubernetes system in under 60 seconds.
Platform:
charms.keystone_k8s.v0.identity_resource
-
- Last updated 17 Oct 2023
- Revision Library version 0.4
"""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
```
A sample ops request can be of format
{
"id": <request id>
"tag": <string to identify request>
"ops": [
{
"name": <op name>,
"params": {
<param 1>: <value 1>,
<param 2>: <value 2>
}
}
]
}
For any sensitive data in the ops params, the charm can create secrets and pass
secret id instead of sensitive data as part of ops request. The charm should
ensure to grant secret access to provider charm i.e., keystone over relation.
The secret content should hold the sensitive data with same name as param name.
"""
import json
import logging
from typing import (
Optional,
)
from ops.charm import (
CharmBase,
RelationBrokenEvent,
RelationChangedEvent,
RelationEvent,
RelationJoinedEvent,
)
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 = 4
REQUEST_NOT_SENT = 1
REQUEST_SENT = 2
REQUEST_PROCESSED = 3
class IdentityOpsProviderReadyEvent(RelationEvent):
"""Has IdentityOpsProviderReady Event."""
pass
class IdentityOpsResponseEvent(RelationEvent):
"""Has IdentityOpsResponse Event."""
pass
class IdentityOpsProviderGoneAwayEvent(RelationEvent):
"""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: CharmBase, relation_name: str):
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: RelationJoinedEvent
):
"""Handle IdentityResource joined."""
self._stored.provider_ready = True
self.on.provider_ready.emit(event.relation)
def _on_identity_resource_relation_changed(
self, event: RelationChangedEvent
):
"""Handle IdentityResource changed."""
id_ = self.response.get("id")
self.save_request_in_store(id_, None, None, REQUEST_PROCESSED)
self.on.response_available.emit(event.relation)
def _on_identity_resource_relation_broken(
self, event: RelationBrokenEvent
):
"""Handle IdentityResource broken."""
self._stored.provider_ready = False
self.on.provider_goneaway.emit(event.relation)
@property
def _identity_resource_rel(self) -> Optional[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
) -> None:
"""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) -> Optional[str]:
"""Return the value for the given key from remote app data."""
if self._identity_resource_rel:
data = self._identity_resource_rel.data[
self._identity_resource_rel.app
]
return data.get(key)
return None
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: CharmBase, relation_name: str):
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: RelationChangedEvent
):
"""Handle IdentityResource changed."""
request = event.relation.data[event.relation.app].get("request")
if request is not None:
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
) -> None:
"""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)