diff --git a/ops-sunbeam/fetch-libs.sh b/ops-sunbeam/fetch-libs.sh index 5e73ba06..e4e15500 100755 --- a/ops-sunbeam/fetch-libs.sh +++ b/ops-sunbeam/fetch-libs.sh @@ -7,7 +7,7 @@ echo "WARNING: Charm interface libs are excluded from ASO python package." charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress charmcraft fetch-lib charms.data_platform_libs.v0.database_requires charmcraft fetch-lib charms.sunbeam_keystone_operator.v0.identity_service -charmcraft fetch-lib charms.sunbeam_keystone_operator.v0.cloud_credentials +charmcraft fetch-lib charms.keystone_k8s.v0.cloud_credentials charmcraft fetch-lib charms.sunbeam_rabbitmq_operator.v0.amqp charmcraft fetch-lib charms.sunbeam_ovn_central_operator.v0.ovsdb charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch diff --git a/ops-sunbeam/ops_sunbeam/relation_handlers.py b/ops-sunbeam/ops_sunbeam/relation_handlers.py index 27b63219..595141ad 100644 --- a/ops-sunbeam/ops_sunbeam/relation_handlers.py +++ b/ops-sunbeam/ops_sunbeam/relation_handlers.py @@ -769,7 +769,7 @@ class CloudCredentialsRequiresHandler(RelationHandler): def setup_event_handler(self) -> ops.charm.Object: """Configure event handlers for cloud-credentials relation.""" - import charms.sunbeam_keystone_operator.v0.cloud_credentials as \ + import charms.keystone_k8s.v0.cloud_credentials as \ cloud_credentials logger.debug('Setting up the cloud-credentials event handler') credentials_service = cloud_credentials.CloudCredentialsRequires( diff --git a/ops-sunbeam/unit_tests/lib/charms/keystone_k8s/v0/cloud_credentials.py b/ops-sunbeam/unit_tests/lib/charms/keystone_k8s/v0/cloud_credentials.py new file mode 100644 index 00000000..0384119a --- /dev/null +++ b/ops-sunbeam/unit_tests/lib/charms/keystone_k8s/v0/cloud_credentials.py @@ -0,0 +1,418 @@ +"""CloudCredentialsProvides and Requires module. + + +This library contains the Requires and Provides classes for handling +the cloud_credentials interface. + +Import `CloudCredentialsRequires` in your charm, with the charm object and the +relation name: + - self + - "cloud_credentials" + +Also provide additional parameters to the charm object: + - service + - internal_url + - public_url + - admin_url + - region + - username + - vhost + +Two events are also available to respond to: + - connected + - ready + - goneaway + +A basic example showing the usage of this relation follows: + +``` +from charms.keystone_k8s.v0.cloud_credentials import CloudCredentialsRequires + +class CloudCredentialsClientCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + # CloudCredentials Requires + self.cloud_credentials = CloudCredentialsRequires( + self, "cloud_credentials", + service = "my-service" + internal_url = "http://internal-url" + public_url = "http://public-url" + admin_url = "http://admin-url" + region = "region" + ) + self.framework.observe( + self.cloud_credentials.on.connected, self._on_cloud_credentials_connected) + self.framework.observe( + self.cloud_credentials.on.ready, self._on_cloud_credentials_ready) + self.framework.observe( + self.cloud_credentials.on.goneaway, self._on_cloud_credentials_goneaway) + + def _on_cloud_credentials_connected(self, event): + '''React to the CloudCredentials connected event. + + This event happens when n CloudCredentials relation is added to the + model before credentials etc have been provided. + ''' + # Do something before the relation is complete + pass + + def _on_cloud_credentials_ready(self, event): + '''React to the CloudCredentials ready event. + + The CloudCredentials interface will use the provided config for the + request to the identity server. + ''' + # CloudCredentials Relation is ready. Do something with the completed relation. + pass + + def _on_cloud_credentials_goneaway(self, event): + '''React to the CloudCredentials goneaway event. + + This event happens when an CloudCredentials relation is removed. + ''' + # CloudCredentials Relation has goneaway. shutdown services or suchlike + pass +``` +""" + +import logging + +from ops.framework import ( + StoredState, + EventBase, + ObjectEvents, + EventSource, + Object, +) +from ops.model import Relation + +# The unique Charmhub library identifier, never change it +LIBID = "a5d96cc2686c47eea554ce2210c2d24e" + +# 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 + +logger = logging.getLogger(__name__) + + +class CloudCredentialsConnectedEvent(EventBase): + """CloudCredentials connected Event.""" + + pass + + +class CloudCredentialsReadyEvent(EventBase): + """CloudCredentials ready for use Event.""" + + pass + + +class CloudCredentialsGoneAwayEvent(EventBase): + """CloudCredentials relation has gone-away Event""" + + pass + + +class CloudCredentialsServerEvents(ObjectEvents): + """Events class for `on`""" + + connected = EventSource(CloudCredentialsConnectedEvent) + ready = EventSource(CloudCredentialsReadyEvent) + goneaway = EventSource(CloudCredentialsGoneAwayEvent) + + +class CloudCredentialsRequires(Object): + """ + CloudCredentialsRequires class + """ + + on = CloudCredentialsServerEvents() + _stored = StoredState() + + def __init__(self, charm, 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_joined, + self._on_cloud_credentials_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_cloud_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_departed, + self._on_cloud_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_cloud_credentials_relation_broken, + ) + + def _on_cloud_credentials_relation_joined(self, event): + """CloudCredentials relation joined.""" + logging.debug("CloudCredentials on_joined") + self.on.connected.emit() + self.request_credentials() + + def _on_cloud_credentials_relation_changed(self, event): + """CloudCredentials relation changed.""" + logging.debug("CloudCredentials on_changed") + try: + self.on.ready.emit() + except AttributeError: + logger.exception('Error when emitting event') + + def _on_cloud_credentials_relation_broken(self, event): + """CloudCredentials relation broken.""" + logging.debug("CloudCredentials on_broken") + self.on.goneaway.emit() + + @property + def _cloud_credentials_rel(self) -> Relation: + """The CloudCredentials relation.""" + return self.framework.model.get_relation(self.relation_name) + + def get_remote_app_data(self, key: str) -> str: + """Return the value for the given key from remote app data.""" + data = self._cloud_credentials_rel.data[self._cloud_credentials_rel.app] + return data.get(key) + + @property + def api_version(self) -> str: + """Return the api_version.""" + return self.get_remote_app_data('api-version') + + @property + def auth_host(self) -> str: + """Return the auth_host.""" + return self.get_remote_app_data('auth-host') + + @property + def auth_port(self) -> str: + """Return the auth_port.""" + return self.get_remote_app_data('auth-port') + + @property + def auth_protocol(self) -> str: + """Return the auth_protocol.""" + return self.get_remote_app_data('auth-protocol') + + @property + def internal_host(self) -> str: + """Return the internal_host.""" + return self.get_remote_app_data('internal-host') + + @property + def internal_port(self) -> str: + """Return the internal_port.""" + return self.get_remote_app_data('internal-port') + + @property + def internal_protocol(self) -> str: + """Return the internal_protocol.""" + return self.get_remote_app_data('internal-protocol') + + @property + def username(self) -> str: + """Return the username.""" + return self.get_remote_app_data('username') + + @property + def password(self) -> str: + """Return the password.""" + return self.get_remote_app_data('password') + + @property + def project_name(self) -> str: + """Return the project name.""" + return self.get_remote_app_data('project-name') + + @property + def project_id(self) -> str: + """Return the project id.""" + return self.get_remote_app_data('project-id') + + @property + def user_domain_name(self) -> str: + """Return the name of the user domain.""" + return self.get_remote_app_data('user-domain-name') + + @property + def user_domain_id(self) -> str: + """Return the id of the user domain.""" + return self.get_remote_app_data('user-domain-id') + + @property + def project_domain_name(self) -> str: + """Return the name of the project domain.""" + return self.get_remote_app_data('project-domain-name') + + @property + def project_domain_id(self) -> str: + """Return the id of the project domain.""" + return self.get_remote_app_data('project-domain-id') + + @property + def region(self) -> str: + """Return the region for the auth urls.""" + return self.get_remote_app_data('region') + + def request_credentials(self) -> None: + """Request credentials from the CloudCredentials server.""" + if self.model.unit.is_leader(): + logging.debug(f'Requesting credentials for {self.charm.app.name}') + app_data = self._cloud_credentials_rel.data[self.charm.app] + app_data['username'] = self.charm.app.name + + +class HasCloudCredentialsClientsEvent(EventBase): + """Has CloudCredentialsClients Event.""" + + pass + + +class ReadyCloudCredentialsClientsEvent(EventBase): + """CloudCredentialsClients Ready Event.""" + + def __init__(self, handle, relation_id, relation_name, username): + super().__init__(handle) + self.relation_id = relation_id + self.relation_name = relation_name + self.username = username + + def snapshot(self): + return { + "relation_id": self.relation_id, + "relation_name": self.relation_name, + "username": self.username, + } + + def restore(self, snapshot): + super().restore(snapshot) + self.relation_id = snapshot["relation_id"] + self.relation_name = snapshot["relation_name"] + self.username = snapshot["username"] + + +class CloudCredentialsClientsGoneAwayEvent(EventBase): + """Has CloudCredentialsClientsGoneAwayEvent Event.""" + + pass + + +class CloudCredentialsClientEvents(ObjectEvents): + """Events class for `on`""" + + has_cloud_credentials_clients = EventSource( + HasCloudCredentialsClientsEvent + ) + ready_cloud_credentials_clients = EventSource( + ReadyCloudCredentialsClientsEvent + ) + cloud_credentials_clients_gone = EventSource( + CloudCredentialsClientsGoneAwayEvent + ) + + +class CloudCredentialsProvides(Object): + """ + CloudCredentialsProvides class + """ + + on = CloudCredentialsClientEvents() + _stored = StoredState() + + 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_joined, + self._on_cloud_credentials_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_cloud_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_cloud_credentials_relation_broken, + ) + + def _on_cloud_credentials_relation_joined(self, event): + """Handle CloudCredentials joined.""" + logging.debug("CloudCredentialsProvides on_joined") + self.on.has_cloud_credentials_clients.emit() + + def _on_cloud_credentials_relation_changed(self, event): + """Handle CloudCredentials changed.""" + logging.debug("CloudCredentials on_changed") + REQUIRED_KEYS = ['username'] + + values = [ + event.relation.data[event.relation.app].get(k) + for k in REQUIRED_KEYS + ] + # Validate data on the relation + if all(values): + username = event.relation.data[event.relation.app]['username'] + self.on.ready_cloud_credentials_clients.emit( + event.relation.id, + event.relation.name, + username, + ) + + def _on_cloud_credentials_relation_broken(self, event): + """Handle CloudCredentials broken.""" + logging.debug("CloudCredentialsProvides on_departed") + self.on.cloud_credentials_clients_gone.emit() + + def set_cloud_credentials(self, relation_name: int, + relation_id: str, + api_version: str, + auth_host: str, + auth_port: str, + auth_protocol: str, + internal_host: str, + internal_port: str, + internal_protocol: str, + username: str, + password: str, + project_name: str, + project_id: str, + user_domain_name: str, + user_domain_id: str, + project_domain_name: str, + project_domain_id: str, + region: str): + logging.debug("Setting cloud_credentials connection information.") + _cloud_credentials_rel = None + for relation in self.framework.model.relations[relation_name]: + if relation.id == relation_id: + _cloud_credentials_rel = relation + if not _cloud_credentials_rel: + # Relation has disappeared so don't send the data + return + app_data = _cloud_credentials_rel.data[self.charm.app] + app_data["api-version"] = api_version + app_data["auth-host"] = auth_host + app_data["auth-port"] = str(auth_port) + app_data["auth-protocol"] = auth_protocol + app_data["internal-host"] = internal_host + app_data["internal-port"] = str(internal_port) + app_data["internal-protocol"] = internal_protocol + app_data["username"] = username + app_data["password"] = password + app_data["project-name"] = project_name + app_data["project-id"] = project_id + app_data["user-domain-name"] = user_domain_name + app_data["user-domain-id"] = user_domain_id + app_data["project-domain-name"] = project_domain_name + app_data["project-domain-id"] = project_domain_id + app_data["region"] = region