diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 8ef84fc..0000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 99 -select: E,W,F,C,N -exclude: - venv - .git - build - dist - *.egg_info diff --git a/.jujuignore b/.jujuignore deleted file mode 100644 index 6ccd559..0000000 --- a/.jujuignore +++ /dev/null @@ -1,3 +0,0 @@ -/venv -*.py[cod] -*.charm diff --git a/.stestr.conf b/.stestr.conf deleted file mode 100644 index e4750de..0000000 --- a/.stestr.conf +++ /dev/null @@ -1,3 +0,0 @@ -[DEFAULT] -test_path=./tests/unit -top_dir=./tests diff --git a/.zuul.yaml b/.zuul.yaml index c7711b8..e7c200a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,11 +1,3 @@ - project: templates: - - openstack-python3-charm-jobs - - openstack-cover-jobs - - microk8s-func-test - vars: - charm_build_name: barbican-k8s - juju_channel: 3.2/stable - juju_classic_mode: false - microk8s_channel: 1.28-strict/stable - microk8s_classic_mode: false + - noop-jobs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 2969183..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,33 +0,0 @@ -# Contributing - -To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup). - -You can use the environments created by `tox` for development: - -```shell -tox --notest -e unit -source .tox/unit/bin/activate -``` - -## Testing - -This project uses `tox` for managing test environments. There are some pre-configured environments -that can be used for linting and formatting code when you're preparing contributions to the charm: - -```shell -tox -e fmt # update your code according to linting rules -tox -e lint # code style -tox -e unit # unit tests -tox -e integration # integration tests -tox # runs 'lint' and 'unit' environments -``` - -## Build the charm - -Build the charm in this git repository using: - -```shell -charmcraft pack -``` - - - -[contributors-guide]: https://opendev.org/openstack/charm-barbican-k8s/src/branch/main/CONTRIBUTING.md -[juju-docs-actions]: https://jaas.ai/docs/actions -[juju-docs-config-apps]: https://juju.is/docs/configuring-applications -[lp-bugs-charm-barbican-k8s]: https://bugs.launchpad.net/charm-barbican-k8s/+filebug diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..39cdc03 --- /dev/null +++ b/README.rst @@ -0,0 +1,12 @@ +This repository has been merged into the +`Sunbeam Charms `_ +repository. + +The contents of this repository are still available in the Git +source code management system. To see the contents of this +repository before it reached its end of life, please check out the +previous commit with "git checkout HEAD^1". + +For any further questions, please email +openstack-discuss@lists.openstack.org or join #openstack-sunbeam on +OFTC. diff --git a/charmcraft.yaml b/charmcraft.yaml deleted file mode 100644 index 58193c7..0000000 --- a/charmcraft.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# This file configures Charmcraft. -# See https://juju.is/docs/sdk/charmcraft-config for guidance. - -type: charm -bases: - - build-on: - - name: ubuntu - channel: "22.04" - run-on: - - name: ubuntu - channel: "22.04" -parts: - charm: - build-packages: - - git - - libffi-dev - - libssl-dev - charm-binary-python-packages: - - cryptography - - jsonschema - - pydantic<2.0 - - jinja2 - - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/config.yaml b/config.yaml deleted file mode 100644 index 1259ca4..0000000 --- a/config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -options: - debug: - default: False - description: Enable debug logging. - type: boolean - region: - default: RegionOne - description: Space delimited list of OpenStack regions - type: string diff --git a/fetch-libs.sh b/fetch-libs.sh deleted file mode 100755 index a573dd6..0000000 --- a/fetch-libs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "INFO: Fetching libs from charmhub." -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.v0.identity_resource -charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq -charmcraft fetch-lib charms.traefik_k8s.v2.ingress diff --git a/lib/charms/data_platform_libs/v0/database_requires.py b/lib/charms/data_platform_libs/v0/database_requires.py deleted file mode 100644 index 11ffd6c..0000000 --- a/lib/charms/data_platform_libs/v0/database_requires.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -r"""[DEPRECATED] Relation 'requires' side abstraction for database relation. - -This library is a uniform interface to a selection of common database -metadata, with added custom events that add convenience to database management, -and methods to consume the application related data. - -Following an example of using the DatabaseCreatedEvent, in the context of the -application charm code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Charm events defined in the database requires charm library. - self.database = DatabaseRequires(self, relation_name="database", database_name="database") - self.framework.observe(self.database.on.database_created, self._on_database_created) - - def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - - # Start application with rendered configuration - self._start_application(config_file) - - # Set active status - self.unit.status = ActiveStatus("received database credentials") -``` - -As shown above, the library provides some custom events to handle specific situations, -which are listed below: - -— database_created: event emitted when the requested database is created. -— endpoints_changed: event emitted when the read/write endpoints of the database have changed. -— read_only_endpoints_changed: event emitted when the read-only endpoints of the database - have changed. Event is not triggered if read/write endpoints changed too. - -If it is needed to connect multiple database clusters to the same relation endpoint -the application charm can implement the same code as if it would connect to only -one database cluster (like the above code example). - -To differentiate multiple clusters connected to the same relation endpoint -the application charm can use the name of the remote application: - -```python - -def _on_database_created(self, event: DatabaseCreatedEvent) -> None: - # Get the remote app name of the cluster that triggered this event - cluster = event.relation.app.name -``` - -It is also possible to provide an alias for each different database cluster/relation. - -So, it is possible to differentiate the clusters in two ways. -The first is to use the remote application name, i.e., `event.relation.app.name`, as above. - -The second way is to use different event handlers to handle each cluster events. -The implementation would be something like the following code: - -```python - -from charms.data_platform_libs.v0.database_requires import ( - DatabaseCreatedEvent, - DatabaseRequires, -) - -class ApplicationCharm(CharmBase): - # Application charm that connects to database charms. - - def __init__(self, *args): - super().__init__(*args) - - # Define the cluster aliases and one handler for each cluster database created event. - self.database = DatabaseRequires( - self, - relation_name="database", - database_name="database", - relations_aliases = ["cluster1", "cluster2"], - ) - self.framework.observe( - self.database.on.cluster1_database_created, self._on_cluster1_database_created - ) - self.framework.observe( - self.database.on.cluster2_database_created, self._on_cluster2_database_created - ) - - def _on_cluster1_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster1 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - - def _on_cluster2_database_created(self, event: DatabaseCreatedEvent) -> None: - # Handle the created database on the cluster named cluster2 - - # Create configuration file for app - config_file = self._render_app_config_file( - event.username, - event.password, - event.endpoints, - ) - ... - -``` -""" - -import json -import logging -from collections import namedtuple -from datetime import datetime -from typing import List, Optional - -from ops.charm import ( - CharmEvents, - RelationChangedEvent, - RelationEvent, - RelationJoinedEvent, -) -from ops.framework import EventSource, Object -from ops.model import Relation - -# The unique Charmhub library identifier, never change it -LIBID = "0241e088ffa9440fb4e3126349b2fb62" - -# 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 = 6 - -logger = logging.getLogger(__name__) - - -class DatabaseEvent(RelationEvent): - """Base class for database events.""" - - @property - def endpoints(self) -> Optional[str]: - """Returns a comma separated list of read/write endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("endpoints") - - @property - def password(self) -> Optional[str]: - """Returns the password for the created user.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("password") - - @property - def read_only_endpoints(self) -> Optional[str]: - """Returns a comma separated list of read only endpoints.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("read-only-endpoints") - - @property - def replset(self) -> Optional[str]: - """Returns the replicaset name. - - MongoDB only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("replset") - - @property - def tls(self) -> Optional[str]: - """Returns whether TLS is configured.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls") - - @property - def tls_ca(self) -> Optional[str]: - """Returns TLS CA.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("tls-ca") - - @property - def uris(self) -> Optional[str]: - """Returns the connection URIs. - - MongoDB, Redis, OpenSearch and Kafka only. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("uris") - - @property - def username(self) -> Optional[str]: - """Returns the created username.""" - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("username") - - @property - def version(self) -> Optional[str]: - """Returns the version of the database. - - Version as informed by the database daemon. - """ - if not self.relation.app: - return None - - return self.relation.data[self.relation.app].get("version") - - -class DatabaseCreatedEvent(DatabaseEvent): - """Event emitted when a new database is created for use on this relation.""" - - -class DatabaseEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read/write endpoints are changed.""" - - -class DatabaseReadOnlyEndpointsChangedEvent(DatabaseEvent): - """Event emitted when the read only endpoints are changed.""" - - -class DatabaseEvents(CharmEvents): - """Database events. - - This class defines the events that the database can emit. - """ - - database_created = EventSource(DatabaseCreatedEvent) - endpoints_changed = EventSource(DatabaseEndpointsChangedEvent) - read_only_endpoints_changed = EventSource(DatabaseReadOnlyEndpointsChangedEvent) - - -Diff = namedtuple("Diff", "added changed deleted") -Diff.__doc__ = """ -A tuple for storing the diff between two data mappings. - -— added — keys that were added. -— changed — keys that still exist but have new values. -— deleted — keys that were deleted. -""" - - -class DatabaseRequires(Object): - """Requires-side of the database relation.""" - - on = DatabaseEvents() # pyright: ignore [reportGeneralTypeIssues] - - def __init__( - self, - charm, - relation_name: str, - database_name: str, - extra_user_roles: Optional[str] = None, - relations_aliases: Optional[List[str]] = None, - ): - """Manager of database client relations.""" - super().__init__(charm, relation_name) - self.charm = charm - self.database = database_name - self.extra_user_roles = extra_user_roles - self.local_app = self.charm.model.app - self.local_unit = self.charm.unit - self.relation_name = relation_name - self.relations_aliases = relations_aliases - self.framework.observe( - self.charm.on[relation_name].relation_joined, self._on_relation_joined_event - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, self._on_relation_changed_event - ) - - # Define custom event names for each alias. - if relations_aliases: - # Ensure the number of aliases does not exceed the maximum - # of connections allowed in the specific relation. - relation_connection_limit = self.charm.meta.requires[relation_name].limit - if len(relations_aliases) != relation_connection_limit: - raise ValueError( - f"The number of aliases must match the maximum number of connections allowed in the relation. " - f"Expected {relation_connection_limit}, got {len(relations_aliases)}" - ) - - for relation_alias in relations_aliases: - self.on.define_event(f"{relation_alias}_database_created", DatabaseCreatedEvent) - self.on.define_event( - f"{relation_alias}_endpoints_changed", DatabaseEndpointsChangedEvent - ) - self.on.define_event( - f"{relation_alias}_read_only_endpoints_changed", - DatabaseReadOnlyEndpointsChangedEvent, - ) - - def _assign_relation_alias(self, relation_id: int) -> None: - """Assigns an alias to a relation. - - This function writes in the unit data bag. - - Args: - relation_id: the identifier for a particular relation. - """ - # If no aliases were provided, return immediately. - if not self.relations_aliases: - return - - # Return if an alias was already assigned to this relation - # (like when there are more than one unit joining the relation). - if ( - self.charm.model.get_relation(self.relation_name, relation_id) - .data[self.local_unit] - .get("alias") - ): - return - - # Retrieve the available aliases (the ones that weren't assigned to any relation). - available_aliases = self.relations_aliases[:] - for relation in self.charm.model.relations[self.relation_name]: - alias = relation.data[self.local_unit].get("alias") - if alias: - logger.debug("Alias %s was already assigned to relation %d", alias, relation.id) - available_aliases.remove(alias) - - # Set the alias in the unit relation databag of the specific relation. - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_unit].update({"alias": available_aliases[0]}) - - def _diff(self, event: RelationChangedEvent) -> Diff: - """Retrieves the diff of the data in the relation changed databag. - - Args: - event: relation changed event. - - Returns: - a Diff instance containing the added, deleted and changed - keys from the event relation databag. - """ - # Retrieve the old data from the data key in the local unit relation databag. - old_data = json.loads(event.relation.data[self.local_unit].get("data", "{}")) - # Retrieve the new data from the event relation databag. - new_data = ( - {key: value for key, value in event.relation.data[event.app].items() if key != "data"} - if event.app - else {} - ) - - # These are the keys that were added to the databag and triggered this event. - added = new_data.keys() - old_data.keys() - # These are the keys that were removed from the databag and triggered this event. - deleted = old_data.keys() - new_data.keys() - # These are the keys that already existed in the databag, - # but had their values changed. - changed = { - key for key in old_data.keys() & new_data.keys() if old_data[key] != new_data[key] - } - - # TODO: evaluate the possibility of losing the diff if some error - # happens in the charm before the diff is completely checked (DPE-412). - # Convert the new_data to a serializable format and save it for a next diff check. - event.relation.data[self.local_unit].update({"data": json.dumps(new_data)}) - - # Return the diff with all possible changes. - return Diff(added, changed, deleted) - - def _emit_aliased_event(self, event: RelationChangedEvent, event_name: str) -> None: - """Emit an aliased event to a particular relation if it has an alias. - - Args: - event: the relation changed event that was received. - event_name: the name of the event to emit. - """ - alias = self._get_relation_alias(event.relation.id) - if alias: - getattr(self.on, f"{alias}_{event_name}").emit( - event.relation, app=event.app, unit=event.unit - ) - - def _get_relation_alias(self, relation_id: int) -> Optional[str]: - """Returns the relation alias. - - Args: - relation_id: the identifier for a particular relation. - - Returns: - the relation alias or None if the relation was not found. - """ - for relation in self.charm.model.relations[self.relation_name]: - if relation.id == relation_id: - return relation.data[self.local_unit].get("alias") - return None - - def fetch_relation_data(self) -> dict: - """Retrieves data from relation. - - This function can be used to retrieve data from a relation - in the charm code when outside an event callback. - - Returns: - a dict of the values stored in the relation data bag - for all relation instances (indexed by the relation ID). - """ - data = {} - for relation in self.relations: - data[relation.id] = ( - {key: value for key, value in relation.data[relation.app].items() if key != "data"} - if relation.app - else {} - ) - return data - - def _update_relation_data(self, relation_id: int, data: dict) -> None: - """Updates a set of key-value pairs in the relation. - - This function writes in the application data bag, therefore, - only the leader unit can call it. - - Args: - relation_id: the identifier for a particular relation. - data: dict containing the key-value pairs - that should be updated in the relation. - """ - if self.local_unit.is_leader(): - relation = self.charm.model.get_relation(self.relation_name, relation_id) - relation.data[self.local_app].update(data) - - def _on_relation_joined_event(self, event: RelationJoinedEvent) -> None: - """Event emitted when the application joins the database relation.""" - # If relations aliases were provided, assign one to the relation. - self._assign_relation_alias(event.relation.id) - - # Sets both database and extra user roles in the relation - # if the roles are provided. Otherwise, sets only the database. - if self.extra_user_roles: - self._update_relation_data( - event.relation.id, - { - "database": self.database, - "extra-user-roles": self.extra_user_roles, - }, - ) - else: - self._update_relation_data(event.relation.id, {"database": self.database}) - - def _on_relation_changed_event(self, event: RelationChangedEvent) -> None: - """Event emitted when the database relation has changed.""" - # Check which data has changed to emit customs events. - diff = self._diff(event) - - # Check if the database is created - # (the database charm shared the credentials). - if "username" in diff.added and "password" in diff.added: - # Emit the default event (the one without an alias). - logger.info("database created at %s", datetime.now()) - getattr(self.on, "database_created").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "database_created") - - # To avoid unnecessary application restarts do not trigger - # “endpoints_changed“ event if “database_created“ is triggered. - return - - # Emit an endpoints changed event if the database - # added or changed this info in the relation databag. - if "endpoints" in diff.added or "endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("endpoints changed on %s", datetime.now()) - getattr(self.on, "endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "endpoints_changed") - - # To avoid unnecessary application restarts do not trigger - # “read_only_endpoints_changed“ event if “endpoints_changed“ is triggered. - return - - # Emit a read only endpoints changed event if the database - # added or changed this info in the relation databag. - if "read-only-endpoints" in diff.added or "read-only-endpoints" in diff.changed: - # Emit the default event (the one without an alias). - logger.info("read-only-endpoints changed on %s", datetime.now()) - getattr(self.on, "read_only_endpoints_changed").emit( - event.relation, app=event.app, unit=event.unit - ) - - # Emit the aliased event (if any). - self._emit_aliased_event(event, "read_only_endpoints_changed") - - @property - def relations(self) -> List[Relation]: - """The list of Relation instances associated with this relation_name.""" - return list(self.charm.model.relations[self.relation_name]) diff --git a/lib/charms/keystone_k8s/v0/identity_resource.py b/lib/charms/keystone_k8s/v0/identity_resource.py deleted file mode 100644 index 154fab8..0000000 --- a/lib/charms/keystone_k8s/v0/identity_resource.py +++ /dev/null @@ -1,392 +0,0 @@ -"""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": - "tag": - "ops": [ - { - "name": , - "params": { - : , - : - } - } - ] -} - -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 = 3 - - -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", {}) - 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) diff --git a/lib/charms/keystone_k8s/v1/identity_service.py b/lib/charms/keystone_k8s/v1/identity_service.py deleted file mode 100644 index 62dd9a3..0000000 --- a/lib/charms/keystone_k8s/v1/identity_service.py +++ /dev/null @@ -1,525 +0,0 @@ -"""IdentityServiceProvides and Requires module. - - -This library contains the Requires and Provides classes for handling -the identity_service interface. - -Import `IdentityServiceRequires` in your charm, with the charm object and the -relation name: - - self - - "identity_service" - -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.v1.identity_service import IdentityServiceRequires - -class IdentityServiceClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # IdentityService Requires - self.identity_service = IdentityServiceRequires( - self, "identity_service", - service = "my-service" - internal_url = "http://internal-url" - public_url = "http://public-url" - admin_url = "http://admin-url" - region = "region" - ) - self.framework.observe( - self.identity_service.on.connected, self._on_identity_service_connected) - self.framework.observe( - self.identity_service.on.ready, self._on_identity_service_ready) - self.framework.observe( - self.identity_service.on.goneaway, self._on_identity_service_goneaway) - - def _on_identity_service_connected(self, event): - '''React to the IdentityService connected event. - - This event happens when n IdentityService relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_identity_service_ready(self, event): - '''React to the IdentityService ready event. - - The IdentityService interface will use the provided config for the - request to the identity server. - ''' - # IdentityService Relation is ready. Do something with the completed relation. - pass - - def _on_identity_service_goneaway(self, event): - '''React to the IdentityService goneaway event. - - This event happens when an IdentityService relation is removed. - ''' - # IdentityService Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -import json -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) -from ops.model import ( - Relation, - SecretNotFoundError, -) - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0fa7fe7236c14c6e9624acf232b9a3b0" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# 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 IdentityServiceConnectedEvent(EventBase): - """IdentityService connected Event.""" - - pass - - -class IdentityServiceReadyEvent(EventBase): - """IdentityService ready for use Event.""" - - pass - - -class IdentityServiceGoneAwayEvent(EventBase): - """IdentityService relation has gone-away Event""" - - pass - - -class IdentityServiceServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(IdentityServiceConnectedEvent) - ready = EventSource(IdentityServiceReadyEvent) - goneaway = EventSource(IdentityServiceGoneAwayEvent) - - -class IdentityServiceRequires(Object): - """ - IdentityServiceRequires class - """ - - on = IdentityServiceServerEvents() - _stored = StoredState() - - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """IdentityService relation joined.""" - logging.debug("IdentityService on_joined") - self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) - - def _on_identity_service_relation_changed(self, event): - """IdentityService relation changed.""" - logging.debug("IdentityService on_changed") - try: - self.service_password - self.on.ready.emit() - except (AttributeError, KeyError): - pass - - def _on_identity_service_relation_broken(self, event): - """IdentityService relation broken.""" - logging.debug("IdentityService on_broken") - self.on.goneaway.emit() - - @property - def _identity_service_rel(self) -> Relation: - """The IdentityService 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._identity_service_rel.data[self._identity_service_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 admin_domain_name(self) -> str: - """Return the admin_domain_name.""" - return self.get_remote_app_data('admin-domain-name') - - @property - def admin_domain_id(self) -> str: - """Return the admin_domain_id.""" - return self.get_remote_app_data('admin-domain-id') - - @property - def admin_project_name(self) -> str: - """Return the admin_project_name.""" - return self.get_remote_app_data('admin-project-name') - - @property - def admin_project_id(self) -> str: - """Return the admin_project_id.""" - return self.get_remote_app_data('admin-project-id') - - @property - def admin_user_name(self) -> str: - """Return the admin_user_name.""" - return self.get_remote_app_data('admin-user-name') - - @property - def admin_user_id(self) -> str: - """Return the admin_user_id.""" - return self.get_remote_app_data('admin-user-id') - - @property - def service_domain_name(self) -> str: - """Return the service_domain_name.""" - return self.get_remote_app_data('service-domain-name') - - @property - def service_domain_id(self) -> str: - """Return the service_domain_id.""" - return self.get_remote_app_data('service-domain-id') - - @property - def service_host(self) -> str: - """Return the service_host.""" - return self.get_remote_app_data('service-host') - - @property - def service_credentials(self) -> str: - """Return the service_credentials secret.""" - return self.get_remote_app_data('service-credentials') - - @property - def service_password(self) -> str: - """Return the service_password.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("password") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_port(self) -> str: - """Return the service_port.""" - return self.get_remote_app_data('service-port') - - @property - def service_protocol(self) -> str: - """Return the service_protocol.""" - return self.get_remote_app_data('service-protocol') - - @property - def service_project_name(self) -> str: - """Return the service_project_name.""" - return self.get_remote_app_data('service-project-name') - - @property - def service_project_id(self) -> str: - """Return the service_project_id.""" - return self.get_remote_app_data('service-project-id') - - @property - def service_user_name(self) -> str: - """Return the service_user_name.""" - credentials_id = self.get_remote_app_data('service-credentials') - if not credentials_id: - return None - - try: - credentials = self.charm.model.get_secret(id=credentials_id) - return credentials.get_content().get("username") - except SecretNotFoundError: - logger.warning(f"Secret {credentials_id} not found") - return None - - @property - def service_user_id(self) -> str: - """Return the service_user_id.""" - return self.get_remote_app_data('service-user-id') - - @property - def internal_auth_url(self) -> str: - """Return the internal_auth_url.""" - return self.get_remote_app_data('internal-auth-url') - - @property - def admin_auth_url(self) -> str: - """Return the admin_auth_url.""" - return self.get_remote_app_data('admin-auth-url') - - @property - def public_auth_url(self) -> str: - """Return the public_auth_url.""" - return self.get_remote_app_data('public-auth-url') - - @property - def admin_role(self) -> str: - """Return the admin_role.""" - return self.get_remote_app_data('admin-role') - - def register_services(self, service_endpoints: dict, - region: str) -> None: - """Request access to the IdentityService server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting service registration") - app_data = self._identity_service_rel.data[self.charm.app] - app_data["service-endpoints"] = json.dumps( - service_endpoints, sort_keys=True - ) - app_data["region"] = region - - -class HasIdentityServiceClientsEvent(EventBase): - """Has IdentityServiceClients Event.""" - - pass - - -class ReadyIdentityServiceClientsEvent(EventBase): - """IdentityServiceClients Ready Event.""" - - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.service_endpoints = service_endpoints - self.region = region - self.client_app_name = client_app_name - - def snapshot(self): - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "service_endpoints": self.service_endpoints, - "client_app_name": self.client_app_name, - "region": self.region} - - def restore(self, snapshot): - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.service_endpoints = snapshot["service_endpoints"] - self.region = snapshot["region"] - self.client_app_name = snapshot["client_app_name"] - - -class IdentityServiceClientEvents(ObjectEvents): - """Events class for `on`""" - - has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) - - -class IdentityServiceProvides(Object): - """ - IdentityServiceProvides class - """ - - on = IdentityServiceClientEvents() - _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_identity_service_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_identity_service_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_identity_service_relation_broken, - ) - - def _on_identity_service_relation_joined(self, event): - """Handle IdentityService joined.""" - logging.debug("IdentityService on_joined") - self.on.has_identity_service_clients.emit() - - def _on_identity_service_relation_changed(self, event): - """Handle IdentityService changed.""" - logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] - - values = [ - event.relation.data[event.relation.app].get(k) - for k in REQUIRED_KEYS - ] - # Validate data on the relation - if all(values): - service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) - self.on.ready_identity_service_clients.emit( - event.relation.id, - event.relation.name, - service_eps, - event.relation.data[event.relation.app]['region'], - event.relation.app.name) - - def _on_identity_service_relation_broken(self, event): - """Handle IdentityService broken.""" - logging.debug("IdentityServiceProvides on_departed") - # TODO clear data on the relation - - def set_identity_service_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, - service_host: str, - service_port: str, - service_protocol: str, - admin_domain: str, - admin_project: str, - admin_user: str, - service_domain: str, - service_project: str, - service_user: str, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): - logging.debug("Setting identity_service connection information.") - _identity_service_rel = None - for relation in self.framework.model.relations[relation_name]: - if relation.id == relation_id: - _identity_service_rel = relation - if not _identity_service_rel: - # Relation has disappeared so skip send of data - return - app_data = _identity_service_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["service-host"] = service_host - app_data["service-port"] = str(service_port) - app_data["service-protocol"] = service_protocol - app_data["admin-domain-name"] = admin_domain.name - app_data["admin-domain-id"] = admin_domain.id - app_data["admin-project-name"] = admin_project.name - app_data["admin-project-id"] = admin_project.id - app_data["admin-user-name"] = admin_user.name - app_data["admin-user-id"] = admin_user.id - app_data["service-domain-name"] = service_domain.name - app_data["service-domain-id"] = service_domain.id - app_data["service-project-name"] = service_project.name - app_data["service-project-id"] = service_project.id - app_data["service-user-id"] = service_user.id - app_data["internal-auth-url"] = internal_auth_url - app_data["admin-auth-url"] = admin_auth_url - app_data["public-auth-url"] = public_auth_url - app_data["service-credentials"] = service_credentials - app_data["admin-role"] = admin_role diff --git a/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/lib/charms/rabbitmq_k8s/v0/rabbitmq.py deleted file mode 100644 index c7df240..0000000 --- a/lib/charms/rabbitmq_k8s/v0/rabbitmq.py +++ /dev/null @@ -1,286 +0,0 @@ -"""RabbitMQProvides and Requires module. - -This library contains the Requires and Provides classes for handling -the rabbitmq interface. - -Import `RabbitMQRequires` in your charm, with the charm object and the -relation name: - - self - - "amqp" - -Also provide two additional parameters to the charm object: - - 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.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires - -class RabbitMQClientCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - # RabbitMQ Requires - self.amqp = RabbitMQRequires( - self, "amqp", - username="myusername", - vhost="vhostname" - ) - self.framework.observe( - self.amqp.on.connected, self._on_amqp_connected) - self.framework.observe( - self.amqp.on.ready, self._on_amqp_ready) - self.framework.observe( - self.amqp.on.goneaway, self._on_amqp_goneaway) - - def _on_amqp_connected(self, event): - '''React to the RabbitMQ connected event. - - This event happens when n RabbitMQ relation is added to the - model before credentials etc have been provided. - ''' - # Do something before the relation is complete - pass - - def _on_amqp_ready(self, event): - '''React to the RabbitMQ ready event. - - The RabbitMQ interface will use the provided username and vhost for the - request to the rabbitmq server. - ''' - # RabbitMQ Relation is ready. Do something with the completed relation. - pass - - def _on_amqp_goneaway(self, event): - '''React to the RabbitMQ goneaway event. - - This event happens when an RabbitMQ relation is removed. - ''' - # RabbitMQ Relation has goneaway. shutdown services or suchlike - pass -``` -""" - -# The unique Charmhub library identifier, never change it -LIBID = "45622352791142fd9cf87232e3bd6f2a" - -# 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 - -import logging - -from ops.framework import ( - StoredState, - EventBase, - ObjectEvents, - EventSource, - Object, -) - -from ops.model import Relation - -from typing import List - -logger = logging.getLogger(__name__) - - -class RabbitMQConnectedEvent(EventBase): - """RabbitMQ connected Event.""" - - pass - - -class RabbitMQReadyEvent(EventBase): - """RabbitMQ ready for use Event.""" - - pass - - -class RabbitMQGoneAwayEvent(EventBase): - """RabbitMQ relation has gone-away Event""" - - pass - - -class RabbitMQServerEvents(ObjectEvents): - """Events class for `on`""" - - connected = EventSource(RabbitMQConnectedEvent) - ready = EventSource(RabbitMQReadyEvent) - goneaway = EventSource(RabbitMQGoneAwayEvent) - - -class RabbitMQRequires(Object): - """ - RabbitMQRequires class - """ - - on = RabbitMQServerEvents() - - def __init__(self, charm, relation_name: str, username: str, vhost: str): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.username = username - self.vhost = vhost - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_departed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """RabbitMQ relation joined.""" - logging.debug("RabbitMQRabbitMQRequires on_joined") - self.on.connected.emit() - self.request_access(self.username, self.vhost) - - def _on_amqp_relation_changed(self, event): - """RabbitMQ relation changed.""" - logging.debug("RabbitMQRabbitMQRequires on_changed/departed") - if self.password: - self.on.ready.emit() - - def _on_amqp_relation_broken(self, event): - """RabbitMQ relation broken.""" - logging.debug("RabbitMQRabbitMQRequires on_broken") - self.on.goneaway.emit() - - @property - def _amqp_rel(self) -> Relation: - """The RabbitMQ relation.""" - return self.framework.model.get_relation(self.relation_name) - - @property - def password(self) -> str: - """Return the RabbitMQ password from the server side of the relation.""" - return self._amqp_rel.data[self._amqp_rel.app].get("password") - - @property - def hostname(self) -> str: - """Return the hostname from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("hostname") - - @property - def ssl_port(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") - - @property - def ssl_ca(self) -> str: - """Return the SSL port from the RabbitMQ relation""" - return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") - - @property - def hostnames(self) -> List[str]: - """Return a list of remote RMQ hosts from the RabbitMQ relation""" - _hosts = [] - for unit in self._amqp_rel.units: - _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) - return _hosts - - def request_access(self, username: str, vhost: str) -> None: - """Request access to the RabbitMQ server.""" - if self.model.unit.is_leader(): - logging.debug("Requesting RabbitMQ user and vhost") - self._amqp_rel.data[self.charm.app]["username"] = username - self._amqp_rel.data[self.charm.app]["vhost"] = vhost - - -class HasRabbitMQClientsEvent(EventBase): - """Has RabbitMQClients Event.""" - - pass - - -class ReadyRabbitMQClientsEvent(EventBase): - """RabbitMQClients Ready Event.""" - - pass - - -class RabbitMQClientEvents(ObjectEvents): - """Events class for `on`""" - - has_amqp_clients = EventSource(HasRabbitMQClientsEvent) - ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) - - -class RabbitMQProvides(Object): - """ - RabbitMQProvides class - """ - - on = RabbitMQClientEvents() - - def __init__(self, charm, relation_name, callback): - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.callback = callback - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_amqp_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_amqp_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_amqp_relation_broken, - ) - - def _on_amqp_relation_joined(self, event): - """Handle RabbitMQ joined.""" - logging.debug("RabbitMQRabbitMQProvides on_joined data={}" - .format(event.relation.data[event.relation.app])) - self.on.has_amqp_clients.emit() - - def _on_amqp_relation_changed(self, event): - """Handle RabbitMQ changed.""" - logging.debug("RabbitMQRabbitMQProvides on_changed data={}" - .format(event.relation.data[event.relation.app])) - # Validate data on the relation - if self.username(event) and self.vhost(event): - self.on.ready_amqp_clients.emit() - if self.charm.unit.is_leader(): - self.callback(event, self.username(event), self.vhost(event)) - else: - logging.warning("Received RabbitMQ changed event without the " - "expected keys ('username', 'vhost') in the " - "application data bag. Incompatible charm in " - "other end of relation?") - - def _on_amqp_relation_broken(self, event): - """Handle RabbitMQ broken.""" - logging.debug("RabbitMQRabbitMQProvides on_departed") - # TODO clear data on the relation - - def username(self, event): - """Return the RabbitMQ username from the client side of the relation.""" - return event.relation.data[event.relation.app].get("username") - - def vhost(self, event): - """Return the RabbitMQ vhost from the client side of the relation.""" - return event.relation.data[event.relation.app].get("vhost") diff --git a/lib/charms/traefik_k8s/v2/ingress.py b/lib/charms/traefik_k8s/v2/ingress.py deleted file mode 100644 index 0364c8a..0000000 --- a/lib/charms/traefik_k8s/v2/ingress.py +++ /dev/null @@ -1,734 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -r"""# Interface Library for ingress. - -This library wraps relation endpoints using the `ingress` interface -and provides a Python API for both requesting and providing per-application -ingress, with load-balancing occurring across all units. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. - -```shell -cd some-charm -charmcraft fetch-lib charms.traefik_k8s.v1.ingress -``` - -In the `metadata.yaml` of the charm, add the following: - -```yaml -requires: - ingress: - interface: ingress - limit: 1 -``` - -Then, to initialise the library: - -```python -from charms.traefik_k8s.v2.ingress import (IngressPerAppRequirer, - IngressPerAppReadyEvent, IngressPerAppRevokedEvent) - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - self.ingress = IngressPerAppRequirer(self, port=80) - # The following event is triggered when the ingress URL to be used - # by this deployment of the `SomeCharm` is ready (or changes). - self.framework.observe( - self.ingress.on.ready, self._on_ingress_ready - ) - self.framework.observe( - self.ingress.on.revoked, self._on_ingress_revoked - ) - - def _on_ingress_ready(self, event: IngressPerAppReadyEvent): - logger.info("This app's ingress URL: %s", event.url) - - def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): - logger.info("This app no longer has ingress") -""" -import json -import logging -import socket -import typing -from dataclasses import dataclass -from typing import ( - Any, - Dict, - List, - MutableMapping, - Optional, - Sequence, - Tuple, -) - -import pydantic -from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent -from ops.framework import EventSource, Object, ObjectEvents, StoredState -from ops.model import ModelError, Relation, Unit -from pydantic import AnyHttpUrl, BaseModel, Field, validator - -# The unique Charmhub library identifier, never change it -LIBID = "e6de2a5cd5b34422a204668f3b8f90d2" - -# Increment this major API version when introducing breaking changes -LIBAPI = 2 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 6 - -PYDEPS = ["pydantic<2.0"] - -DEFAULT_RELATION_NAME = "ingress" -RELATION_INTERFACE = "ingress" - -log = logging.getLogger(__name__) -BUILTIN_JUJU_KEYS = {"ingress-address", "private-address", "egress-subnets"} - - -class DatabagModel(BaseModel): - """Base databag model.""" - - class Config: - """Pydantic config.""" - - allow_population_by_field_name = True - """Allow instantiating this class by field name (instead of forcing alias).""" - - _NEST_UNDER = None - - @classmethod - def load(cls, databag: MutableMapping): - """Load this model from a Juju databag.""" - if cls._NEST_UNDER: - return cls.parse_obj(json.loads(databag[cls._NEST_UNDER])) - - try: - data = {k: json.loads(v) for k, v in databag.items() if k not in BUILTIN_JUJU_KEYS} - except json.JSONDecodeError as e: - msg = f"invalid databag contents: expecting json. {databag}" - log.error(msg) - raise DataValidationError(msg) from e - - try: - return cls.parse_raw(json.dumps(data)) # type: ignore - except pydantic.ValidationError as e: - msg = f"failed to validate databag: {databag}" - log.error(msg, exc_info=True) - raise DataValidationError(msg) from e - - def dump(self, databag: Optional[MutableMapping] = None, clear: bool = True): - """Write the contents of this model to Juju databag. - - :param databag: the databag to write the data to. - :param clear: ensure the databag is cleared before writing it. - """ - if clear and databag: - databag.clear() - - if databag is None: - databag = {} - - if self._NEST_UNDER: - databag[self._NEST_UNDER] = self.json() - - dct = self.dict() - for key, field in self.__fields__.items(): # type: ignore - value = dct[key] - databag[field.alias or key] = json.dumps(value) - - return databag - - -# todo: import these models from charm-relation-interfaces/ingress/v2 instead of redeclaring them -class IngressUrl(BaseModel): - """Ingress url schema.""" - - url: AnyHttpUrl - - -class IngressProviderAppData(DatabagModel): - """Ingress application databag schema.""" - - ingress: IngressUrl - - -class ProviderSchema(BaseModel): - """Provider schema for Ingress.""" - - app: IngressProviderAppData - - -class IngressRequirerAppData(DatabagModel): - """Ingress requirer application databag model.""" - - model: str = Field(description="The model the application is in.") - name: str = Field(description="the name of the app requesting ingress.") - port: int = Field(description="The port the app wishes to be exposed.") - - # fields on top of vanilla 'ingress' interface: - strip_prefix: Optional[bool] = Field( - description="Whether to strip the prefix from the ingress url.", alias="strip-prefix" - ) - redirect_https: Optional[bool] = Field( - description="Whether to redirect http traffic to https.", alias="redirect-https" - ) - - scheme: Optional[str] = Field( - default="http", description="What scheme to use in the generated ingress url" - ) - - @validator("scheme", pre=True) - def validate_scheme(cls, scheme): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate scheme arg.""" - if scheme not in {"http", "https", "h2c"}: - raise ValueError("invalid scheme: should be one of `http|https|h2c`") - return scheme - - @validator("port", pre=True) - def validate_port(cls, port): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate port.""" - assert isinstance(port, int), type(port) - assert 0 < port < 65535, "port out of TCP range" - return port - - -class IngressRequirerUnitData(DatabagModel): - """Ingress requirer unit databag model.""" - - host: str = Field(description="Hostname the unit wishes to be exposed.") - - @validator("host", pre=True) - def validate_host(cls, host): # noqa: N805 # pydantic wants 'cls' as first arg - """Validate host.""" - assert isinstance(host, str), type(host) - return host - - -class RequirerSchema(BaseModel): - """Requirer schema for Ingress.""" - - app: IngressRequirerAppData - unit: IngressRequirerUnitData - - -class IngressError(RuntimeError): - """Base class for custom errors raised by this library.""" - - -class NotReadyError(IngressError): - """Raised when a relation is not ready.""" - - -class DataValidationError(IngressError): - """Raised when data validation fails on IPU relation data.""" - - -class _IngressPerAppBase(Object): - """Base class for IngressPerUnit interface classes.""" - - def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME): - super().__init__(charm, relation_name) - - self.charm: CharmBase = charm - self.relation_name = relation_name - self.app = self.charm.app - self.unit = self.charm.unit - - observe = self.framework.observe - rel_events = charm.on[relation_name] - observe(rel_events.relation_created, self._handle_relation) - observe(rel_events.relation_joined, self._handle_relation) - observe(rel_events.relation_changed, self._handle_relation) - observe(rel_events.relation_broken, self._handle_relation_broken) - observe(charm.on.leader_elected, self._handle_upgrade_or_leader) # type: ignore - observe(charm.on.upgrade_charm, self._handle_upgrade_or_leader) # type: ignore - - @property - def relations(self): - """The list of Relation instances associated with this endpoint.""" - return list(self.charm.model.relations[self.relation_name]) - - def _handle_relation(self, event): - """Subclasses should implement this method to handle a relation update.""" - pass - - def _handle_relation_broken(self, event): - """Subclasses should implement this method to handle a relation breaking.""" - pass - - def _handle_upgrade_or_leader(self, event): - """Subclasses should implement this method to handle upgrades or leadership change.""" - pass - - -class _IPAEvent(RelationEvent): - __args__: Tuple[str, ...] = () - __optional_kwargs__: Dict[str, Any] = {} - - @classmethod - def __attrs__(cls): - return cls.__args__ + tuple(cls.__optional_kwargs__.keys()) - - def __init__(self, handle, relation, *args, **kwargs): - super().__init__(handle, relation) - - if not len(self.__args__) == len(args): - raise TypeError("expected {} args, got {}".format(len(self.__args__), len(args))) - - for attr, obj in zip(self.__args__, args): - setattr(self, attr, obj) - for attr, default in self.__optional_kwargs__.items(): - obj = kwargs.get(attr, default) - setattr(self, attr, obj) - - def snapshot(self): - dct = super().snapshot() - for attr in self.__attrs__(): - obj = getattr(self, attr) - try: - dct[attr] = obj - except ValueError as e: - raise ValueError( - "cannot automagically serialize {}: " - "override this method and do it " - "manually.".format(obj) - ) from e - - return dct - - def restore(self, snapshot) -> None: - super().restore(snapshot) - for attr, obj in snapshot.items(): - setattr(self, attr, obj) - - -class IngressPerAppDataProvidedEvent(_IPAEvent): - """Event representing that ingress data has been provided for an app.""" - - __args__ = ("name", "model", "hosts", "strip_prefix", "redirect_https") - - if typing.TYPE_CHECKING: - name: Optional[str] = None - model: Optional[str] = None - # sequence of hostname, port dicts - hosts: Sequence["IngressRequirerUnitData"] = () - strip_prefix: bool = False - redirect_https: bool = False - - -class IngressPerAppDataRemovedEvent(RelationEvent): - """Event representing that ingress data has been removed for an app.""" - - -class IngressPerAppProviderEvents(ObjectEvents): - """Container for IPA Provider events.""" - - data_provided = EventSource(IngressPerAppDataProvidedEvent) - data_removed = EventSource(IngressPerAppDataRemovedEvent) - - -@dataclass -class IngressRequirerData: - """Data exposed by the ingress requirer to the provider.""" - - app: "IngressRequirerAppData" - units: List["IngressRequirerUnitData"] - - -class TlsProviderType(typing.Protocol): - """Placeholder.""" - - @property - def enabled(self) -> bool: # type: ignore - """Placeholder.""" - - -class IngressPerAppProvider(_IngressPerAppBase): - """Implementation of the provider of ingress.""" - - on = IngressPerAppProviderEvents() # type: ignore - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - ): - """Constructor for IngressPerAppProvider. - - Args: - charm: The charm that is instantiating the instance. - relation_name: The name of the relation endpoint to bind to - (defaults to "ingress"). - """ - super().__init__(charm, relation_name) - - def _handle_relation(self, event): - # created, joined or changed: if remote side has sent the required data: - # notify listeners. - if self.is_ready(event.relation): - data = self.get_data(event.relation) - self.on.data_provided.emit( # type: ignore - event.relation, - data.app.name, - data.app.model, - [unit.dict() for unit in data.units], - data.app.strip_prefix or False, - data.app.redirect_https or False, - ) - - def _handle_relation_broken(self, event): - self.on.data_removed.emit(event.relation) # type: ignore - - def wipe_ingress_data(self, relation: Relation): - """Clear ingress data from relation.""" - assert self.unit.is_leader(), "only leaders can do this" - try: - relation.data - except ModelError as e: - log.warning( - "error {} accessing relation data for {!r}. " - "Probably a ghost of a dead relation is still " - "lingering around.".format(e, relation.name) - ) - return - del relation.data[self.app]["ingress"] - - def _get_requirer_units_data(self, relation: Relation) -> List["IngressRequirerUnitData"]: - """Fetch and validate the requirer's app databag.""" - out: List["IngressRequirerUnitData"] = [] - - unit: Unit - for unit in relation.units: - databag = relation.data[unit] - try: - data = IngressRequirerUnitData.load(databag) - out.append(data) - except pydantic.ValidationError: - log.info(f"failed to validate remote unit data for {unit}") - raise - return out - - @staticmethod - def _get_requirer_app_data(relation: Relation) -> "IngressRequirerAppData": - """Fetch and validate the requirer's app databag.""" - app = relation.app - if app is None: - raise NotReadyError(relation) - - databag = relation.data[app] - return IngressRequirerAppData.load(databag) - - def get_data(self, relation: Relation) -> IngressRequirerData: - """Fetch the remote (requirer) app and units' databags.""" - try: - return IngressRequirerData( - self._get_requirer_app_data(relation), self._get_requirer_units_data(relation) - ) - except (pydantic.ValidationError, DataValidationError) as e: - raise DataValidationError("failed to validate ingress requirer data") from e - - def is_ready(self, relation: Optional[Relation] = None): - """The Provider is ready if the requirer has sent valid data.""" - if not relation: - return any(map(self.is_ready, self.relations)) - - try: - self.get_data(relation) - except (DataValidationError, NotReadyError) as e: - log.debug("Provider not ready; validation error encountered: %s" % str(e)) - return False - return True - - def _published_url(self, relation: Relation) -> Optional["IngressProviderAppData"]: - """Fetch and validate this app databag; return the ingress url.""" - if not self.is_ready(relation) or not self.unit.is_leader(): - # Handle edge case where remote app name can be missing, e.g., - # relation_broken events. - # Also, only leader units can read own app databags. - # FIXME https://github.com/canonical/traefik-k8s-operator/issues/34 - return None - - # fetch the provider's app databag - databag = relation.data[self.app] - if not databag.get("ingress"): - raise NotReadyError("This application did not `publish_url` yet.") - - return IngressProviderAppData.load(databag) - - def publish_url(self, relation: Relation, url: str): - """Publish to the app databag the ingress url.""" - ingress_url = {"url": url} - IngressProviderAppData.parse_obj({"ingress": ingress_url}).dump(relation.data[self.app]) - - @property - def proxied_endpoints(self) -> Dict[str, str]: - """Returns the ingress settings provided to applications by this IngressPerAppProvider. - - For example, when this IngressPerAppProvider has provided the - `http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary - will be: - - ``` - { - "my-app": { - "url": "http://foo.bar/my-model.my-app" - } - } - ``` - """ - results = {} - - for ingress_relation in self.relations: - if not ingress_relation.app: - log.warning( - f"no app in relation {ingress_relation} when fetching proxied endpoints: skipping" - ) - continue - try: - ingress_data = self._published_url(ingress_relation) - except NotReadyError: - log.warning( - f"no published url found in {ingress_relation}: " - f"traefik didn't publish_url yet to this relation." - ) - continue - - if not ingress_data: - log.warning(f"relation {ingress_relation} not ready yet: try again in some time.") - continue - - results[ingress_relation.app.name] = ingress_data.ingress.dict() - return results - - -class IngressPerAppReadyEvent(_IPAEvent): - """Event representing that ingress for an app is ready.""" - - __args__ = ("url",) - if typing.TYPE_CHECKING: - url: Optional[str] = None - - -class IngressPerAppRevokedEvent(RelationEvent): - """Event representing that ingress for an app has been revoked.""" - - -class IngressPerAppRequirerEvents(ObjectEvents): - """Container for IPA Requirer events.""" - - ready = EventSource(IngressPerAppReadyEvent) - revoked = EventSource(IngressPerAppRevokedEvent) - - -class IngressPerAppRequirer(_IngressPerAppBase): - """Implementation of the requirer of the ingress relation.""" - - on = IngressPerAppRequirerEvents() # type: ignore - - # used to prevent spurious urls to be sent out if the event we're currently - # handling is a relation-broken one. - _stored = StoredState() - - def __init__( - self, - charm: CharmBase, - relation_name: str = DEFAULT_RELATION_NAME, - *, - host: Optional[str] = None, - port: Optional[int] = None, - strip_prefix: bool = False, - redirect_https: bool = False, - # fixme: this is horrible UX. - # shall we switch to manually calling provide_ingress_requirements with all args when ready? - scheme: typing.Callable[[], str] = lambda: "http", - ): - """Constructor for IngressRequirer. - - The request args can be used to specify the ingress properties when the - instance is created. If any are set, at least `port` is required, and - they will be sent to the ingress provider as soon as it is available. - All request args must be given as keyword args. - - Args: - charm: the charm that is instantiating the library. - relation_name: the name of the relation endpoint to bind to (defaults to `ingress`); - relation must be of interface type `ingress` and have "limit: 1") - host: Hostname to be used by the ingress provider to address the requiring - application; if unspecified, the default Kubernetes service name will be used. - strip_prefix: configure Traefik to strip the path prefix. - redirect_https: redirect incoming requests to HTTPS. - scheme: callable returning the scheme to use when constructing the ingress url. - - Request Args: - port: the port of the service - """ - super().__init__(charm, relation_name) - self.charm: CharmBase = charm - self.relation_name = relation_name - self._strip_prefix = strip_prefix - self._redirect_https = redirect_https - self._get_scheme = scheme - - self._stored.set_default(current_url=None) # type: ignore - - # if instantiated with a port, and we are related, then - # we immediately publish our ingress data to speed up the process. - if port: - self._auto_data = host, port - else: - self._auto_data = None - - def _handle_relation(self, event): - # created, joined or changed: if we have auto data: publish it - self._publish_auto_data() - - if self.is_ready(): - # Avoid spurious events, emit only when there is a NEW URL available - new_url = ( - None - if isinstance(event, RelationBrokenEvent) - else self._get_url_from_relation_data() - ) - if self._stored.current_url != new_url: # type: ignore - self._stored.current_url = new_url # type: ignore - self.on.ready.emit(event.relation, new_url) # type: ignore - - def _handle_relation_broken(self, event): - self._stored.current_url = None # type: ignore - self.on.revoked.emit(event.relation) # type: ignore - - def _handle_upgrade_or_leader(self, event): - """On upgrade/leadership change: ensure we publish the data we have.""" - self._publish_auto_data() - - def is_ready(self): - """The Requirer is ready if the Provider has sent valid data.""" - try: - return bool(self._get_url_from_relation_data()) - except DataValidationError as e: - log.debug("Requirer not ready; validation error encountered: %s" % str(e)) - return False - - def _publish_auto_data(self): - if self._auto_data: - host, port = self._auto_data - self.provide_ingress_requirements(host=host, port=port) - - def provide_ingress_requirements( - self, - *, - scheme: Optional[str] = None, - host: Optional[str] = None, - port: int, - ): - """Publishes the data that Traefik needs to provide ingress. - - Args: - scheme: Scheme to be used; if unspecified, use the one used by __init__. - host: Hostname to be used by the ingress provider to address the - requirer unit; if unspecified, FQDN will be used instead - port: the port of the service (required) - """ - for relation in self.relations: - self._provide_ingress_requirements(scheme, host, port, relation) - - def _provide_ingress_requirements( - self, - scheme: Optional[str], - host: Optional[str], - port: int, - relation: Relation, - ): - if self.unit.is_leader(): - self._publish_app_data(scheme, port, relation) - - self._publish_unit_data(host, relation) - - def _publish_unit_data( - self, - host: Optional[str], - relation: Relation, - ): - if not host: - host = socket.getfqdn() - - unit_databag = relation.data[self.unit] - try: - IngressRequirerUnitData(host=host).dump(unit_databag) - except pydantic.ValidationError as e: - msg = "failed to validate unit data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - def _publish_app_data( - self, - scheme: Optional[str], - port: int, - relation: Relation, - ): - # assumes leadership! - app_databag = relation.data[self.app] - - if not scheme: - # If scheme was not provided, use the one given to the constructor. - scheme = self._get_scheme() - - try: - IngressRequirerAppData( # type: ignore # pyright does not like aliases - model=self.model.name, - name=self.app.name, - scheme=scheme, - port=port, - strip_prefix=self._strip_prefix, # type: ignore # pyright does not like aliases - redirect_https=self._redirect_https, # type: ignore # pyright does not like aliases - ).dump(app_databag) - except pydantic.ValidationError as e: - msg = "failed to validate app data" - log.info(msg, exc_info=True) # log to INFO because this might be expected - raise DataValidationError(msg) from e - - @property - def relation(self): - """The established Relation instance, or None.""" - return self.relations[0] if self.relations else None - - def _get_url_from_relation_data(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - relation = self.relation - if not relation or not relation.app: - return None - - # fetch the provider's app databag - try: - databag = relation.data[relation.app] - except ModelError as e: - log.debug( - f"Error {e} attempting to read remote app data; " - f"probably we are in a relation_departed hook" - ) - return None - - if not databag: # not ready yet - return None - - return str(IngressProviderAppData.load(databag).ingress.url) - - @property - def url(self) -> Optional[str]: - """The full ingress URL to reach the current unit. - - Returns None if the URL isn't available yet. - """ - data = ( - typing.cast(Optional[str], self._stored.current_url) # type: ignore - or self._get_url_from_relation_data() - ) - return data diff --git a/lib/charms/vault_k8s/v0/vault_kv.py b/lib/charms/vault_k8s/v0/vault_kv.py deleted file mode 100644 index f3929db..0000000 --- a/lib/charms/vault_k8s/v0/vault_kv.py +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -"""Library for the vault-kv relation. - -This library contains the Requires and Provides classes for handling the vault-kv -interface. - -## Getting Started -From a charm directory, fetch the library using `charmcraft`: - -```shell -charmcraft fetch-lib charms.vault_k8s.v0.vault_kv -``` - -### Requirer charm -The requirer charm is the charm requiring a secret value store. In this example, the requirer charm -is requiring a secret value store. - -```python -import secrets - -from charms.vault_k8s.v0 import vault_kv -from ops.charm import CharmBase, InstallEvent -from ops.main import main -from ops.model import ActiveStatus, BlockedStatus - -NONCE_SECRET_LABEL = "nonce" - - -class ExampleRequirerCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - self.interface = vault_kv.VaultKvRequires( - self, - "vault-kv", - "my-suffix", - ) - - self.framework.observe(self.on.install, self._on_install) - self.framework.observe(self.interface.on.connected, self._on_connected) - self.framework.observe(self.interface.on.ready, self._on_ready) - self.framework.observe(self.interface.on.gone_away, self._on_gone_away) - self.framework.observe(self.on.update_status, self._on_update_status) - - def _on_install(self, event: InstallEvent): - self.unit.add_secret( - {"nonce": secrets.token_hex(16)}, - label=NONCE_SECRET_LABEL, - description="Nonce for vault-kv relation", - ) - self.unit.status = BlockedStatus("Waiting for vault-kv relation") - - def _on_connected(self, event: vault_kv.VaultKvConnectedEvent): - relation = self.model.get_relation(event.relation_name, event.relation_id) - egress_subnet = str(self.model.get_binding(relation).network.interfaces[0].subnet) - self.interface.request_credentials(relation, egress_subnet, self.get_nonce()) - - def _on_ready(self, event: vault_kv.VaultKvReadyEvent): - relation = self.model.get_relation(event.relation_name, event.relation_id) - if relation is None: - return - vault_url = self.interface.get_vault_url(relation) - ca_certificate = self.interface.get_ca_certificate(relation) - mount = self.interface.get_mount(relation) - - unit_credentials = self.interface.get_unit_credentials(relation) - # unit_credentials is a juju secret id - secret = self.model.get_secret(id=unit_credentials) - secret_content = secret.get_content() - role_id = secret_content["role-id"] - role_secret_id = secret_content["role-secret-id"] - - self._configure(vault_url, ca_certificate, mount, role_id, role_secret_id) - - self.unit.status = ActiveStatus() - - def _on_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent): - self.unit.status = BlockedStatus("Waiting for vault-kv relation") - - def _configure( - self, - vault_url: str, - ca_certificate: str, - mount: str, - role_id: str, - role_secret_id: str, - ): - pass - - def _on_update_status(self, event): - # Check somewhere that egress subnet has not changed i.e. pod has not been rescheduled - # Update status might not be the best place - binding = self.model.get_binding("vault-kv") - if binding is not None: - egress_subnet = str(binding.network.interfaces[0].subnet) - self.interface.request_credentials(event.relation, egress_subnet, self.get_nonce()) - - def get_nonce(self): - secret = self.model.get_secret(label=NONCE_SECRET_LABEL) - nonce = secret.get_content()["nonce"] - return nonce - - -if __name__ == "__main__": - main(ExampleRequirerCharm) -``` - -You can integrate both charms by running: - -```bash -juju integrate -``` -""" - -import json -import logging -from typing import Any, Dict, Iterable, Mapping, Optional, Union - -import ops -from interface_tester.schema_base import DataBagSchema # type: ignore[import] -from pydantic import BaseModel, Field, Json, ValidationError - -logger = logging.getLogger(__name__) - - -# The unique Charmhub library identifier, never change it -LIBID = "591d6d2fb6a54853b4bb53ef16ef603a" - -# 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 - -PYDEPS = ["pydantic", "pytest-interface-tester"] - - -class VaultKvProviderSchema(BaseModel): - """Provider side of the vault-kv interface.""" - - vault_url: str = Field(description="The URL of the Vault server to connect to.") - mount: str = Field( - description=( - "The KV mount available for the requirer application, " - "respecting the pattern 'charm--'." - ) - ) - ca_certificate: str = Field( - description="The CA certificate to use when validating the Vault server's certificate." - ) - credentials: Json[Mapping[str, str]] = Field( - description=( - "Mapping of unit name and credentials for that unit." - " Credentials are a juju secret containing a 'role-id' and a 'role-secret-id'." - ) - ) - - -class AppVaultKvRequirerSchema(BaseModel): - """App schema of the requirer side of the vault-kv interface.""" - - mount_suffix: str = Field( - description="Suffix to append to the mount name to get the KV mount." - ) - - -class UnitVaultKvRequirerSchema(BaseModel): - """Unit schema of the requirer side of the vault-kv interface.""" - - egress_subnet: str = Field(description="Egress subnet to use, in CIDR notation.") - nonce: str = Field( - description="Uniquely identifying value for this unit. `secrets.token_hex(16)` is recommended." - ) - - -class ProviderSchema(DataBagSchema): - """The schema for the provider side of this interface.""" - - app: VaultKvProviderSchema - - -class RequirerSchema(DataBagSchema): - """The schema for the requirer side of this interface.""" - - app: AppVaultKvRequirerSchema - unit: UnitVaultKvRequirerSchema - - -def is_requirer_data_valid(app_data: dict, unit_data: dict) -> bool: - """Return whether the requirer data is valid.""" - try: - RequirerSchema( - app=AppVaultKvRequirerSchema(**app_data), - unit=UnitVaultKvRequirerSchema(**unit_data), - ) - return True - except ValidationError as e: - logger.debug("Invalid data: %s", e) - return False - - -def is_provider_data_valid(data: dict) -> bool: - """Return whether the provider data is valid.""" - try: - ProviderSchema(app=VaultKvProviderSchema(**data)) - return True - except ValidationError as e: - logger.debug("Invalid data: %s", e) - return False - - -class NewVaultKvClientAttachedEvent(ops.EventBase): - """New vault kv client attached event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - mount_suffix: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - self.mount_suffix = mount_suffix - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - "mount_suffix": self.mount_suffix, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - self.mount_suffix = snapshot["mount_suffix"] - - -class VaultKvProviderEvents(ops.ObjectEvents): - """List of events that the Vault Kv provider charm can leverage.""" - - new_vault_kv_client_attached = ops.EventSource(NewVaultKvClientAttachedEvent) - - -class VaultKvProvides(ops.Object): - """Class to be instanciated by the providing side of the relation.""" - - on = VaultKvProviderEvents() - - def __init__( - self, - charm: ops.CharmBase, - relation_name: str, - ) -> None: - 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_relation_changed, - ) - - def _on_relation_changed(self, event: ops.RelationChangedEvent): - """Handle client changed relation. - - This handler will emit a new_vault_kv_client_attached event if at least one unit data is - valid. - """ - if event.app is None: - logger.debug("No remote application yet") - return - - app_data = dict(event.relation.data[event.app]) - - any_valid = False - for unit in event.relation.units: - if not is_requirer_data_valid(app_data, dict(event.relation.data[unit])): - logger.debug("Invalid data from unit %r", unit.name) - continue - any_valid = True - - if any_valid: - self.on.new_vault_kv_client_attached.emit( - event.relation.id, - event.relation.name, - event.relation.data[event.app]["mount_suffix"], - ) - - def set_vault_url(self, relation: ops.Relation, vault_url: str): - """Set the vault_url on the relation.""" - if not self.charm.unit.is_leader(): - return - - relation.data[self.charm.app]["vault_url"] = vault_url - - def set_ca_certificate(self, relation: ops.Relation, ca_certificate: str): - """Set the ca_certificate on the relation.""" - if not self.charm.unit.is_leader(): - return - - relation.data[self.charm.app]["ca_certificate"] = ca_certificate - - def set_mount(self, relation: ops.Relation, mount: str): - """Set the mount on the relation.""" - if not self.charm.unit.is_leader(): - return - - relation.data[self.charm.app]["mount"] = mount - - def set_unit_credentials(self, relation: ops.Relation, nonce: str, secret: ops.Secret): - """Set the unit credentials on the relation.""" - if not self.charm.unit.is_leader(): - return - - credentials = self.get_credentials(relation) - if secret.id is None: - logger.debug( - "Secret id is None, not updating the relation '%s:%d' for nonce %r", - relation.name, - relation.id, - nonce, - ) - return - credentials[nonce] = secret.id - relation.data[self.charm.app]["credentials"] = json.dumps(credentials, sort_keys=True) - - def remove_unit_credentials(self, relation: ops.Relation, nonce: Union[str, Iterable[str]]): - """Remove nonce(s) from the relation.""" - if not self.charm.unit.is_leader(): - return - - if isinstance(nonce, str): - nonce = [nonce] - - credentials = self.get_credentials(relation) - - for n in nonce: - credentials.pop(n, None) - - relation.data[self.charm.app]["credentials"] = json.dumps(credentials, sort_keys=True) - - def get_credentials(self, relation: ops.Relation) -> dict: - """Get the unit credentials from the relation.""" - return json.loads(relation.data[self.charm.app].get("credentials", "{}")) - - -class VaultKvConnectedEvent(ops.EventBase): - """VaultKvConnectedEvent Event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class VaultKvReadyEvent(ops.EventBase): - """VaultKvReadyEvent Event.""" - - def __init__( - self, - handle: ops.Handle, - relation_id: int, - relation_name: str, - ): - super().__init__(handle) - self.relation_id = relation_id - self.relation_name = relation_name - - def snapshot(self) -> dict: - """Return snapshot data that should be persisted.""" - return { - "relation_id": self.relation_id, - "relation_name": self.relation_name, - } - - def restore(self, snapshot: Dict[str, Any]): - """Restore the value state from a given snapshot.""" - super().restore(snapshot) - self.relation_id = snapshot["relation_id"] - self.relation_name = snapshot["relation_name"] - - -class VaultKvGoneAwayEvent(ops.EventBase): - """VaultKvGoneAwayEvent Event.""" - - pass - - -class VaultKvRequireEvents(ops.ObjectEvents): - """List of events that the Vault Kv requirer charm can leverage.""" - - connected = ops.EventSource(VaultKvConnectedEvent) - ready = ops.EventSource(VaultKvReadyEvent) - gone_away = ops.EventSource(VaultKvGoneAwayEvent) - - -class VaultKvRequires(ops.Object): - """Class to be instanciated by the requiring side of the relation.""" - - on = VaultKvRequireEvents() - - def __init__( - self, - charm: ops.CharmBase, - relation_name: str, - mount_suffix: str, - ) -> None: - super().__init__(charm, relation_name) - self.charm = charm - self.relation_name = relation_name - self.mount_suffix = mount_suffix - self.framework.observe( - self.charm.on[relation_name].relation_joined, - self._on_vault_kv_relation_joined, - ) - self.framework.observe( - self.charm.on[relation_name].relation_changed, - self._on_vault_kv_relation_changed, - ) - self.framework.observe( - self.charm.on[relation_name].relation_broken, - self._on_vault_kv_relation_broken, - ) - - def _set_unit_nonce(self, relation: ops.Relation, nonce: str): - """Set the nonce on the relation.""" - relation.data[self.charm.unit]["nonce"] = nonce - - def _set_unit_egress_subnet(self, relation: ops.Relation, egress_subnet: str): - """Set the egress_subnet on the relation.""" - relation.data[self.charm.unit]["egress_subnet"] = egress_subnet - - def _on_vault_kv_relation_joined(self, event: ops.RelationJoinedEvent): - """Handle relation joined. - - Set the secret backend in the application databag if we are the leader. - Always update the egress_subnet in the unit databag. - """ - if self.charm.unit.is_leader(): - event.relation.data[self.charm.app]["mount_suffix"] = self.mount_suffix - self.on.connected.emit( - event.relation.id, - event.relation.name, - ) - - def _on_vault_kv_relation_changed(self, event: ops.RelationChangedEvent): - """Handle relation changed.""" - if event.app is None: - logger.debug("No remote application yet") - return - - if ( - is_provider_data_valid(dict(event.relation.data[event.app])) - and self.get_unit_credentials(event.relation) is not None - ): - self.on.ready.emit( - event.relation.id, - event.relation.name, - ) - - def _on_vault_kv_relation_broken(self, event: ops.RelationBrokenEvent): - """Handle relation broken.""" - self.on.gone_away.emit() - - def request_credentials(self, relation: ops.Relation, egress_subnet: str, nonce: str) -> None: - """Request credentials from the vault-kv relation. - - Generated secret ids are tied to the unit egress_subnet, so if the egress_subnet - changes a new secret id must be generated. - - A change in egress_subnet can happen when the pod is rescheduled to a different - node by the underlying substrate without a change from Juju. - """ - self._set_unit_egress_subnet(relation, egress_subnet) - self._set_unit_nonce(relation, nonce) - - def get_vault_url(self, relation: ops.Relation) -> Optional[str]: - """Return the vault_url from the relation.""" - if relation.app is None: - return None - return relation.data[relation.app].get("vault_url") - - def get_ca_certificate(self, relation: ops.Relation) -> Optional[str]: - """Return the ca_certificate from the relation.""" - if relation.app is None: - return None - return relation.data[relation.app].get("ca_certificate") - - def get_mount(self, relation: ops.Relation) -> Optional[str]: - """Return the mount from the relation.""" - if relation.app is None: - return None - return relation.data[relation.app].get("mount") - - def get_unit_credentials(self, relation: ops.Relation) -> Optional[str]: - """Return the unit credentials from the relation. - - Unit credentials are stored in the relation data as a Juju secret id. - """ - nonce = relation.data[self.charm.unit].get("nonce") - if nonce is None or relation.app is None: - return None - return json.loads(relation.data[relation.app].get("credentials", "{}")).get(nonce) diff --git a/metadata.yaml b/metadata.yaml deleted file mode 100644 index e0f8d8a..0000000 --- a/metadata.yaml +++ /dev/null @@ -1,64 +0,0 @@ -name: barbican-k8s -display-name: Barbican -summary: Openstack Key Manager service -description: | - Barbican is the OpenStack Key Manager service. - It provides secure storage, provisioning and management of secret data. - This includes keying material such as Symmetric Keys, Asymmetric Keys, Certificates and raw binary data. -maintainer: Openstack Charmers -source: https://opendev.org/openstack/charm-barbican-k8s -issues: https://bugs.launchpad.net/charm-barbican-k8s - -bases: - - name: ubuntu - channel: 22.04/stable - -assumes: - - k8s-api - - juju >= 3.1 -tags: - - openstack - - secrets - - misc - -requires: - ingress-internal: - interface: ingress - limit: 1 - optional: true - ingress-public: - interface: ingress - limit: 1 - database: - interface: mysql_client - limit: 1 - identity-service: - interface: keystone - identity-ops: - interface: keystone-resources - optional: true - amqp: - interface: rabbitmq - vault-kv: - interface: vault-kv - limit: 1 - -peers: - peers: - interface: barbican-peer - -containers: - barbican-api: - resource: barbican-api-image - barbican-worker: - resource: barbican-worker-image - -resources: - barbican-api-image: - type: oci-image - description: OCI image for OpenStack Barbican API - upstream-source: ghcr.io/canonical/barbican-consolidated:2023.2 - barbican-worker-image: - type: oci-image - description: OCI image for OpenStack Barbican worker - upstream-source: ghcr.io/canonical/barbican-consolidated:2023.2 diff --git a/osci.yaml b/osci.yaml deleted file mode 100644 index 7e1239d..0000000 --- a/osci.yaml +++ /dev/null @@ -1,10 +0,0 @@ -- project: - templates: - - charm-publish-jobs - vars: - needs_charm_build: true - charm_build_name: barbican-k8s - build_type: charmcraft - publish_charm: true - charmcraft_channel: 2.0/stable - publish_channel: 2023.2/edge diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 2896bc0..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. - -# Testing tools configuration -[tool.coverage.run] -branch = true - -[tool.coverage.report] -show_missing = true - -[tool.pytest.ini_options] -minversion = "6.0" -log_cli_level = "INFO" - -# Formatting tools configuration -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -multi_line_output = 3 -force_grid_wrap = true - -# Linting tools configuration -[tool.flake8] -max-line-length = 79 -max-doc-length = 99 -max-complexity = 10 -exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] -select = ["E", "W", "F", "C", "N", "R", "D", "H"] -# Ignore W503, E501 because using black creates errors with this -# Ignore D107 Missing docstring in __init__ -ignore = ["W503", "E501", "D107", "E402"] -per-file-ignores = [] -docstring-convention = "google" -# Check for properly formatted copyright header in each file -copyright-check = "True" -copyright-author = "Canonical Ltd." -copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/rename.sh b/rename.sh deleted file mode 100755 index d0c35c9..0000000 --- a/rename.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') -echo "renaming ${charm}_*.charm to ${charm}.charm" -echo -n "pwd: " -pwd -ls -al -echo "Removing bad downloaded charm maybe?" -if [[ -e "${charm}.charm" ]]; -then - rm "${charm}.charm" -fi -echo "Renaming charm here." -mv ${charm}_*.charm ${charm}.charm diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 0f23be4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -cryptography -jinja2 -jsonschema -pydantic<2.0 -lightkube -lightkube-models -ops -pwgen -pytest-interface-tester -git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/src/charm.py b/src/charm.py deleted file mode 100755 index 8d77b65..0000000 --- a/src/charm.py +++ /dev/null @@ -1,494 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Barbican Operator Charm. - -This charm provide Barbican services as part of an OpenStack deployment -""" -import hashlib -import json -import logging -import secrets -from typing import ( - List, - Optional, -) - -import charms.keystone_k8s.v0.identity_resource as identity_resource -import ops -import ops.framework -import ops_sunbeam.charm as sunbeam_charm -import ops_sunbeam.config_contexts as sunbeam_ctxts -import ops_sunbeam.container_handlers as sunbeam_chandlers -import ops_sunbeam.core as sunbeam_core -import ops_sunbeam.relation_handlers as sunbeam_rhandlers -from charms.vault_k8s.v0 import ( - vault_kv, -) -from ops import ( - framework, - model, - pebble, -) -from ops.main import ( - main, -) - -logger = logging.getLogger(__name__) - -BARBICAN_API_CONTAINER = "barbican-api" -BARBICAN_WORKER_CONTAINER = "barbican-worker" -VAULT_KV_RELATION = "vault-kv" -NONCE_SECRET_LABEL = "nonce" - - -class NoRelationError(Exception): - """No relation found.""" - - pass - - -class WSGIBarbicanAdminConfigContext(sunbeam_ctxts.ConfigContext): - """Configuration context for WSGI configuration.""" - - def context(self) -> dict: - """WSGI configuration options.""" - return { - "name": self.charm.service_name, - "public_port": 9312, - "user": self.charm.service_user, - "group": self.charm.service_group, - "wsgi_admin_script": "/usr/bin/barbican-wsgi-api", - "wsgi_public_script": "/usr/bin/barbican-wsgi-api", - "error_log": "/dev/stdout", - "custom_log": "/dev/stdout", - } - - -class VaultKvRequiresHandler(sunbeam_rhandlers.RelationHandler): - """Handler for vault-kv relation.""" - - charm: "BarbicanVaultOperatorCharm" - interface: vault_kv.VaultKvRequires - - def __init__( - self, - charm: ops.CharmBase, - relation_name: str, - callback_f, - mount_suffix: str, - mandatory: bool = False, - ): - self.mount_suffix = mount_suffix - super().__init__(charm, relation_name, callback_f, mandatory) - - def setup_event_handler(self) -> ops.Object: - """Configure event handlers for a vault-kv relation.""" - logger.debug("Setting up vault-kv event handler") - interface = vault_kv.VaultKvRequires( - self.charm, - self.relation_name, - self.mount_suffix, - ) - - self.framework.observe(interface.on.connected, self._on_connected) - self.framework.observe(interface.on.ready, self._on_ready) - self.framework.observe(interface.on.gone_away, self._on_gone_away) - try: - self.request_credentials(interface, self._relation) - except NoRelationError: - pass - return interface - - @property - def _relation(self) -> ops.Relation: - relation = self.model.get_relation(VAULT_KV_RELATION) - if relation is None: - raise NoRelationError("Vault-kv relation not found") - return relation - - def _on_connected(self, event: vault_kv.VaultKvConnectedEvent): - """Handle on connected event.""" - relation = self.model.get_relation( - event.relation_name, event.relation_id - ) - if relation is None: - raise RuntimeError( - "Vault-kv relation not found during a connected event" - ) - self.request_credentials(self.interface, relation) - - def _on_ready(self, event: vault_kv.VaultKvReadyEvent): - """Handle client ready relation.""" - self.callback_f(event) - - def _on_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent): - """Handle client gone away relation.""" - self.callback_f(event) - - def request_credentials( - self, interface: vault_kv.VaultKvRequires, relation: ops.Relation - ): - """Request credentials from vault-kv relation.""" - nonce = self.charm.get_nonce() - if nonce is None: - return - binding = self.model.get_binding(relation) - if binding is None: - logger.debug("No binding found for vault-kv relation") - return - egress_subnet = str(binding.network.interfaces[0].subnet) - interface.request_credentials(relation, egress_subnet, nonce) - - @property - def ready(self) -> bool: - """Whether the handler is ready for use.""" - relation = self.model.get_relation(VAULT_KV_RELATION) - if relation is None: - return False - return all( - ( - self.interface.get_unit_credentials(relation), - self.interface.get_vault_url(relation), - self.interface.get_mount(relation), - ) - ) - - def context(self) -> dict: - """Context containing relation data.""" - vault_kv_relation = self._relation - unit_credentials = self.interface.get_unit_credentials( - vault_kv_relation - ) - if not unit_credentials: - return {} - secret = self.model.get_secret(id=unit_credentials) - secret_content = secret.get_content() - return { - "kv_mountpoint": self.interface.get_mount(vault_kv_relation), - "vault_url": self.interface.get_vault_url(vault_kv_relation), - "approle_role_id": secret_content["role-id"], - "approle_secret_id": secret_content["role-secret-id"], - "ca_crt_file": self.charm.ca_crt_file, - "ca_certificate": self.interface.get_ca_certificate( - vault_kv_relation - ), - } - - -class BarbicanWorkerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): - """Pebble handler for Barbican worker.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.enable_service_check = True - - def get_layer(self) -> dict: - """Barbican worker service layer. - - :returns: pebble layer configuration for worker service - :rtype: dict - """ - return { - "summary": "barbican worker layer", - "description": "pebble configuration for barbican worker", - "services": { - "barbican-worker": { - "override": "replace", - "summary": "Barbican Worker", - "command": "barbican-worker", - "user": "barbican", - "group": "barbican", - } - }, - } - - def default_container_configs( - self, - ) -> List[sunbeam_core.ContainerConfigFile]: - """Container configurations for handler.""" - return [ - sunbeam_core.ContainerConfigFile( - "/etc/barbican/barbican.conf", "barbican", "barbican" - ) - ] - - @property - def service_ready(self) -> bool: - """Determine whether the service the container provides is running.""" - if self.enable_service_check: - logging.debug("Service checks enabled for barbican worker") - return super().service_ready - else: - logging.debug("Service checks disabled for barbican worker") - return self.pebble_ready - - -class BarbicanOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): - """Charm the service.""" - - _state = framework.StoredState() - service_name = "barbican-api" - wsgi_admin_script = "/usr/bin/barbican-wsgi-api" - wsgi_public_script = "/usr/bin/barbican-wsgi-api" - mandatory_relations = { - "database", - "amqp", - "identity-service", - "ingress-public", - } - - db_sync_cmds = [ - ["sudo", "-u", "barbican", "barbican-manage", "db", "upgrade"] - ] - - def configure_unit(self, event: framework.EventBase) -> None: - """Run configuration on this unit.""" - self.disable_barbican_config() - super().configure_unit(event) - - def get_relation_handlers( - self, - handlers: Optional[List[sunbeam_rhandlers.RelationHandler]] = None, - ) -> List[sunbeam_rhandlers.RelationHandler]: - """Relation handlers for the service.""" - handlers = super().get_relation_handlers(handlers) - self.id_ops = sunbeam_rhandlers.IdentityResourceRequiresHandler( - self, - "identity-ops", - self.handle_keystone_ops, - mandatory="identity-ops" in self.mandatory_relations, - ) - handlers.append(self.id_ops) - return handlers - - def handle_keystone_ops(self, event: ops.EventBase) -> None: - """Event handler for identity ops.""" - if isinstance(event, identity_resource.IdentityOpsProviderReadyEvent): - self._state.identity_ops_ready = True - - if not self.unit.is_leader(): - return - - # Send op request only by leader unit - ops = self._get_barbican_role_ops() - id_ = self.hash_ops(ops) - request = { - "id": id_, - "tag": "barbican_roles_setup", - "ops": ops, - } - logger.debug("Sending ops request: %r", request) - self.id_ops.interface.request_ops(request) - elif isinstance( - event, - identity_resource.IdentityOpsProviderGoneAwayEvent, - ): - self._state.identity_ops_ready = False - elif isinstance(event, identity_resource.IdentityOpsResponseEvent): - if not self.unit.is_leader(): - return - response = self.id_ops.interface.response - logger.debug("Got response from keystone: %r", response) - request_tag = response.get("tag") - if request_tag == "barbican_roles_setup": - self._handle_barbican_roles_setup(event) - - def _handle_barbican_roles_setup( - self, - event: ops.EventBase, - ) -> None: - """Handle roles setup response from identity-ops.""" - if { - op.get("return-code") - for op in self.id_ops.interface.response.get( - "ops", - [], - ) - } != {0}: - logger.error("Failed to setup barbican roles") - - def hash_ops(self, ops: list) -> str: - """Return the sha1 of the requested ops.""" - return hashlib.sha1(json.dumps(ops).encode()).hexdigest() - - def _get_barbican_role_ops(self) -> list: - """Generate ops request for domain setup.""" - roles = ["key-manager:service-admin", "creator", "observer", "audit"] - ops = [ - {"name": "create_role", "params": {"name": name}} for name in roles - ] - return ops - - @property - def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]: - """Generate list of configuration adapters for the charm.""" - _cadapters = super().config_contexts - _cadapters.extend( - [ - WSGIBarbicanAdminConfigContext( - self, - "wsgi_barbican_admin", - ) - ] - ) - return _cadapters - - def disable_barbican_config(self): - """Disable default barbican config.""" - container = self.unit.get_container(BARBICAN_API_CONTAINER) - try: - process = container.exec( - ["a2disconf", "barbican-api"], timeout=5 * 60 - ) - out, warnings = process.wait_output() - if warnings: - for line in warnings.splitlines(): - logger.warning("a2disconf warn: %s", line.strip()) - logging.debug(f"Output from a2disconf: \n{out}") - except pebble.ExecError: - logger.exception("Failed to disable barbican-api conf in apache") - self.status = model.ErrorStatus( - "Failed to disable barbican-api conf in apache" - ) - - def get_pebble_handlers( - self, - ) -> List[sunbeam_chandlers.ServicePebbleHandler]: - """Pebble handlers for operator.""" - pebble_handlers = super().get_pebble_handlers() - pebble_handlers.extend( - [ - BarbicanWorkerPebbleHandler( - self, - BARBICAN_WORKER_CONTAINER, - "barbican-worker", - [], - self.template_dir, - self.configure_charm, - ), - ] - ) - return pebble_handlers - - @property - def service_conf(self) -> str: - """Service default configuration file.""" - return "/etc/barbican/barbican.conf" - - @property - def service_user(self) -> str: - """Service user file and directory ownership.""" - return "barbican" - - @property - def service_group(self) -> str: - """Service group file and directory ownership.""" - return "barbican" - - @property - def service_endpoints(self): - """Service endpoints configuration.""" - return [ - { - "service_name": "barbican", - "type": "key-manager", - "description": "OpenStack Barbican API", - "internal_url": f"{self.internal_url}", - "public_url": f"{self.public_url}", - "admin_url": f"{self.admin_url}", - } - ] - - @property - def default_public_ingress_port(self): - """Default port.""" - return 9311 - - @property - def healthcheck_http_url(self) -> str: - """Healthcheck HTTP URL for the service.""" - # / returns a 300 return code, which is not understood by Pebble as OK - return super().healthcheck_http_url + "?build" - - -class BarbicanVaultOperatorCharm(BarbicanOperatorCharm): - """Vault specialized Barbican Operator Charm.""" - - mandatory_relations = BarbicanOperatorCharm.mandatory_relations.union( - {VAULT_KV_RELATION} - ) - - def __init__(self, *args): - super().__init__(*args) - self.framework.observe(self.on.install, self._on_install) - - def _on_install(self, event: ops.framework.EventBase) -> None: - """Handle install event.""" - self.unit.add_secret( - {"nonce": secrets.token_hex(16)}, - label=NONCE_SECRET_LABEL, - description="nonce for vault-kv relation", - ) - - def get_relation_handlers( - self, - handlers: Optional[List[sunbeam_rhandlers.RelationHandler]] = None, - ) -> List[sunbeam_rhandlers.RelationHandler]: - """Relation handlers for the service.""" - handlers = super().get_relation_handlers(handlers) - if self.can_add_handler(VAULT_KV_RELATION, handlers): - self.vault_kv = VaultKvRequiresHandler( - self, - VAULT_KV_RELATION, - self.configure_charm, - self.mount_suffix, - VAULT_KV_RELATION in self.mandatory_relations, - ) - handlers.append(self.vault_kv) - return handlers - - @property - def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: - """Container configuration files for the service.""" - _cconfigs = super().container_configs - _cconfigs.append( - sunbeam_core.ContainerConfigFile( - self.ca_crt_file, "barbican", "barbican" - ) - ) - return _cconfigs - - @property - def mount_suffix(self): - """Secret backend for vault.""" - return "secrets" - - @property - def ca_crt_file(self): - """Vault CA certificate file location.""" - return "/etc/barbican/vault_ca.crt" - - def get_nonce(self) -> Optional[str]: - """Return nonce stored in secret.""" - try: - secret = self.model.get_secret(label=NONCE_SECRET_LABEL) - return secret.get_content()["nonce"] - except ops.SecretNotFoundError: - return None - - -if __name__ == "__main__": - main(BarbicanVaultOperatorCharm) diff --git a/src/templates/barbican.conf b/src/templates/barbican.conf deleted file mode 100644 index 0253b41..0000000 --- a/src/templates/barbican.conf +++ /dev/null @@ -1,40 +0,0 @@ -[DEFAULT] -debug = {{ options.debug }} -lock_path = /var/lock/barbican -state_path = /var/lib/barbican - -host_href = "" -transport_url = {{ amqp.transport_url }} - -sql_connection = {{ database.connection }} -db_auto_create = false - -{% include "parts/section-identity" %} - -{% include "parts/section-service-user" %} - -[secretstore] -{% if vault_kv and vault_kv.approle_role_id -%} -enabled_secretstore_plugins = vault_plugin -{% else -%} -enabled_secretstore_plugins = store_crypto -{% endif -%} - -[crypto] -enabled_crypto_plugins = simple_crypto - -[simple_crypto_plugin] -# the kek should be a 32-byte value which is base64 encoded -kek = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=' - -{% if vault_kv and vault_kv.approle_secret_id -%} -[vault_plugin] -approle_role_id = {{ vault_kv.approle_role_id }} -approle_secret_id = {{ vault_kv.approle_secret_id }} -kv_mountpoint = {{ vault_kv.kv_mountpoint }} -vault_url = {{ vault_kv.vault_url }} -use_ssl = True -ssl_ca_crt_file = {{ vault_kv.ca_crt_file }} -{% endif -%} - -{% include "parts/section-oslo-messaging-rabbit" %} diff --git a/src/templates/parts/section-identity b/src/templates/parts/section-identity deleted file mode 100644 index 92dabb0..0000000 --- a/src/templates/parts/section-identity +++ /dev/null @@ -1,27 +0,0 @@ -[keystone_authtoken] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -interface = admin -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -interface = internal -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -interface = internal -{% endif -%} -{% if identity_service.public_auth_url -%} -www_authenticate_uri = {{ identity_service.public_auth_url }} -{% elif identity_service.internal_host -%} -www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -auth_type = password -project_domain_name = {{ identity_service.service_domain_name }} -user_domain_name = {{ identity_service.service_domain_name }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -service_token_roles = {{ identity_service.admin_role }} -service_token_roles_required = True - -# XXX Region should come from the id relation here -region_name = {{ options.region }} diff --git a/src/templates/parts/section-oslo-messaging-rabbit b/src/templates/parts/section-oslo-messaging-rabbit deleted file mode 100644 index 145c4ee..0000000 --- a/src/templates/parts/section-oslo-messaging-rabbit +++ /dev/null @@ -1,2 +0,0 @@ -[oslo_messaging_rabbit] -rabbit_quorum_queue = True diff --git a/src/templates/parts/section-service-user b/src/templates/parts/section-service-user deleted file mode 100644 index 6510369..0000000 --- a/src/templates/parts/section-service-user +++ /dev/null @@ -1,17 +0,0 @@ -{% if identity_service.service_domain_id -%} -[service_user] -{% if identity_service.admin_auth_url -%} -auth_url = {{ identity_service.admin_auth_url }} -{% elif identity_service.internal_auth_url -%} -auth_url = {{ identity_service.internal_auth_url }} -{% elif identity_service.internal_host -%} -auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} -{% endif -%} -send_service_user_token = true -auth_type = password -project_domain_id = {{ identity_service.service_domain_id }} -user_domain_id = {{ identity_service.service_domain_id }} -project_name = {{ identity_service.service_project_name }} -username = {{ identity_service.service_user_name }} -password = {{ identity_service.service_password }} -{% endif -%} diff --git a/src/templates/vault_ca.crt.j2 b/src/templates/vault_ca.crt.j2 deleted file mode 100644 index d29b416..0000000 --- a/src/templates/vault_ca.crt.j2 +++ /dev/null @@ -1,3 +0,0 @@ -{% if vault_kv is defined and vault_kv.ca_certificate -%} -{{ vault_kv.ca_certificate }} -{% endif -%} diff --git a/src/templates/wsgi-barbican-api.conf b/src/templates/wsgi-barbican-api.conf deleted file mode 100644 index 9070332..0000000 --- a/src/templates/wsgi-barbican-api.conf +++ /dev/null @@ -1,51 +0,0 @@ -Listen {{ wsgi_config.public_port }} -Listen {{ wsgi_barbican_admin.public_port }} - - WSGIDaemonProcess barbican-api processes=4 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \ - display-name=%{GROUP} - WSGIProcessGroup barbican-api - {% if ingress_public and ingress_public.ingress_path -%} - WSGIScriptAlias {{ ingress_public.ingress_path }} {{ wsgi_config.wsgi_public_script }} - {% endif -%} - WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }} - WSGIApplicationGroup %{GLOBAL} - WSGIPassAuthorization On - = 2.4> - ErrorLogFormat "%{cu}t %M" - - ErrorLog {{ wsgi_config.error_log }} - CustomLog {{ wsgi_config.custom_log }} combined - - - = 2.4> - Require all granted - - - Order allow,deny - Allow from all - - - - - WSGIDaemonProcess barbican-admin-api processes=4 threads=1 user={{ wsgi_barbican_admin.user }} group={{ wsgi_barbican_admin.group }} \ - display-name=%{GROUP} - WSGIProcessGroup barbican-admin-api - WSGIScriptAlias / {{ wsgi_barbican_admin.wsgi_public_script }} - WSGIApplicationGroup %{GLOBAL} - WSGIPassAuthorization On - = 2.4> - ErrorLogFormat "%{cu}t %M" - - ErrorLog {{ wsgi_barbican_admin.error_log }} - CustomLog {{ wsgi_barbican_admin.custom_log }} combined - - - = 2.4> - Require all granted - - - Order allow,deny - Allow from all - - - diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 0b8ca0c..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of *requirements.txt files for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools -# - -coverage -mock -flake8 -stestr -git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza -git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack -git+https://opendev.org/openstack/tempest.git#egg=tempest -ops diff --git a/tests/bundles/smoke.yaml b/tests/bundles/smoke.yaml deleted file mode 100644 index 74ba5e9..0000000 --- a/tests/bundles/smoke.yaml +++ /dev/null @@ -1,75 +0,0 @@ -bundle: kubernetes -applications: - mysql: - charm: ch:mysql-k8s - channel: 8.0/stable - scale: 1 - trust: true - - # Currently traefik is required for networking things. - # If this isn't present, the units will hang at "installing agent". - traefik: - charm: ch:traefik-k8s - channel: 1.0/candidate - scale: 1 - trust: true - options: - kubernetes-service-annotations: metallb.universe.tf/address-pool=public - - # required for barbican - rabbitmq: - charm: ch:rabbitmq-k8s - channel: 3.12/edge - scale: 1 - trust: true - options: - minimum-replicas: 1 - - keystone: - charm: ch:keystone-k8s - channel: 2023.2/edge - scale: 1 - trust: false - options: - admin-role: admin - storage: - fernet-keys: 5M - credential-keys: 5M - - vault: - charm: ch:vault-k8s - channel: latest/edge - scale: 1 - trust: false - - barbican: - charm: ../../barbican-k8s.charm - scale: 1 - trust: false - resources: - barbican-api-image: ghcr.io/canonical/barbican-consolidated:2023.2 - barbican-worker-image: ghcr.io/canonical/barbican-consolidated:2023.2 - -relations: -- - traefik:ingress - - keystone:ingress-internal -- - traefik:ingress - - keystone:ingress-public - -- - mysql:database - - keystone:database - -- - mysql:database - - barbican:database -- - rabbitmq:amqp - - barbican:amqp -- - keystone:identity-service - - barbican:identity-service -- - keystone:identity-ops - - barbican:identity-ops -- - traefik:ingress - - barbican:ingress-internal -- - traefik:ingress - - barbican:ingress-public -- - vault:vault-kv - - barbican:vault-kv diff --git a/tests/config.yaml b/tests/config.yaml deleted file mode 120000 index e84e89a..0000000 --- a/tests/config.yaml +++ /dev/null @@ -1 +0,0 @@ -../config.yaml \ No newline at end of file diff --git a/tests/tests.yaml b/tests/tests.yaml deleted file mode 100644 index 48992ad..0000000 --- a/tests/tests.yaml +++ /dev/null @@ -1,38 +0,0 @@ -gate_bundles: - - smoke -smoke_bundles: - - smoke -# There is no storage provider at the moment so cannot run tests. -configure: - - zaza.charm_tests.noop.setup.basic_setup -tests: - - zaza.openstack.charm_tests.barbican.tests.BarbicanTempestTestK8S -tests_options: - trust: - - smoke - ignore_hard_deploy_errors: - - smoke - - tempest: - default: - smoke: True - -target_deploy_status: - traefik: - workload-status: active - workload-status-message-regex: '^$' - rabbitmq: - workload-status: active - workload-status-message-regex: '^$' - keystone: - workload-status: active - workload-status-message-regex: '^$' - mysql: - workload-status: active - workload-status-message-regex: '^.*$' - barbican: - workload-status: active - workload-status-message-regex: '^$' - vault: - workload-status: active - workload-status-message-regex: '^$' diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e058d64..0000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unit tests for Barbican operator.""" diff --git a/tests/unit/test_barbican_charm.py b/tests/unit/test_barbican_charm.py deleted file mode 100644 index eff64ac..0000000 --- a/tests/unit/test_barbican_charm.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2023 Canonical Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unit tests for Barbican operator.""" - -import ops_sunbeam.test_utils as test_utils - -import charm - - -class _BarbicanTestOperatorCharm(charm.BarbicanOperatorCharm): - """Test Operator Charm for Barbican Operator.""" - - def __init__(self, framework): - self.seen_events = [] - super().__init__(framework) - - def _log_event(self, event): - self.seen_events.append(type(event).__name__) - - def configure_charm(self, event): - super().configure_charm(event) - self._log_event(event) - - @property - def public_ingress_address(self): - return "barbican.juju" - - -class TestBarbicanOperatorCharm(test_utils.CharmTestCase): - """Unit tests for Barbican Operator.""" - - PATCHES = [] - - def setUp(self): - """Set up environment for unit test.""" - super().setUp(charm, self.PATCHES) - self.harness = test_utils.get_harness( - _BarbicanTestOperatorCharm, container_calls=self.container_calls - ) - - # clean up events that were dynamically defined, - # otherwise we get issues because they'll be redefined, - # which is not allowed. - from charms.data_platform_libs.v0.database_requires import ( - DatabaseEvents, - ) - - for attr in ( - "database_database_created", - "database_endpoints_changed", - "database_read_only_endpoints_changed", - ): - try: - delattr(DatabaseEvents, attr) - except AttributeError: - pass - - self.addCleanup(self.harness.cleanup) - self.harness.begin() - - def test_pebble_ready_handler(self): - """Test pebble ready handler.""" - self.assertEqual(self.harness.charm.seen_events, []) - test_utils.set_all_pebbles_ready(self.harness) - self.assertEqual(len(self.harness.charm.seen_events), 2) - - def test_all_relations(self): - """Test all integrations for operator.""" - self.harness.set_leader() - test_utils.set_all_pebbles_ready(self.harness) - # this adds all the default/common relations - test_utils.add_all_relations(self.harness) - test_utils.add_complete_ingress_relation(self.harness) - - setup_cmds = [ - ["a2ensite", "wsgi-barbican-api"], - ["sudo", "-u", "barbican", "barbican-manage", "db", "upgrade"], - ] - for cmd in setup_cmds: - self.assertIn(cmd, self.container_calls.execute["barbican-api"]) - config_files = [ - "/etc/apache2/sites-available/wsgi-barbican-api.conf", - "/etc/barbican/barbican.conf", - ] - for f in config_files: - self.check_file("barbican-api", f) - - -def add_db_relation(harness, name) -> str: - """Add db relation.""" - rel_id = harness.add_relation(name, "mysql") - harness.add_relation_unit(rel_id, "mysql/0") - harness.add_relation_unit(rel_id, "mysql/0") - harness.update_relation_data( - rel_id, "mysql/0", {"ingress-address": "10.0.0.3"} - ) - return rel_id diff --git a/tox.ini b/tox.ini deleted file mode 100644 index fbaa02c..0000000 --- a/tox.ini +++ /dev/null @@ -1,169 +0,0 @@ -# Source charm: ./tox.ini -# This file is managed centrally by release-tools and should not be modified -# within individual charm repos. See the 'global' dir contents for available -# choices of tox.ini for OpenStack Charms: -# https://github.com/openstack-charmers/release-tools - -[tox] -skipsdist = True -envlist = pep8,py3 -sitepackages = False -skip_missing_interpreters = False -minversion = 3.18.0 - -[vars] -src_path = {toxinidir}/src/ -tst_path = {toxinidir}/tests/ -lib_path = {toxinidir}/lib/ -pyproject_toml = {toxinidir}/pyproject.toml -all_path = {[vars]src_path} {[vars]tst_path} - -[testenv] -basepython = python3 -setenv = - PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} -passenv = - HOME - PYTHONPATH -install_command = - pip install {opts} {packages} -commands = stestr run --slowest {posargs} -allowlist_externals = - git - charmcraft - {toxinidir}/fetch-libs.sh - {toxinidir}/rename.sh -deps = - -r{toxinidir}/test-requirements.txt - -[testenv:fmt] -description = Apply coding style standards to code -deps = - black - isort -commands = - isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox - black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:build] -basepython = python3 -deps = -commands = - charmcraft -v pack - {toxinidir}/rename.sh - -[testenv:fetch] -basepython = python3 -deps = -commands = - {toxinidir}/fetch-libs.sh - -[testenv:py3] -basepython = python3 -deps = - {[testenv]deps} - -r{toxinidir}/requirements.txt - -[testenv:py38] -basepython = python3.8 -deps = {[testenv:py3]deps} - -[testenv:py39] -basepython = python3.9 -deps = {[testenv:py3]deps} - -[testenv:py310] -basepython = python3.10 -deps = {[testenv:py3]deps} - -[testenv:py311] -basepython = python3.11 -deps = {[testenv:py3]deps} - -[testenv:cover] -basepython = python3 -deps = {[testenv:py3]deps} -setenv = - {[testenv]setenv} - PYTHON=coverage run -commands = - coverage erase - stestr run --slowest {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml - coverage report - -[testenv:pep8] -description = Alias for lint -deps = {[testenv:lint]deps} -commands = {[testenv:lint]commands} - -[testenv:lint] -description = Check code against coding style standards -deps = - black - flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged - flake8-docstrings - flake8-copyright - flake8-builtins - pyproject-flake8 - pep8-naming - isort - codespell -commands = - codespell {[vars]all_path} - # pflake8 wrapper supports config from pyproject.toml - pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} - isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} - black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} - -[testenv:func-noop] -basepython = python3 -deps = - git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza - git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack - git+https://opendev.org/openstack/tempest.git#egg=tempest -commands = - functest-run-suite --help - -[testenv:func] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model - -[testenv:func-smoke] -basepython = python3 -deps = {[testenv:func-noop]deps} -setenv = - TEST_MODEL_SETTINGS = automatically-retry-hooks=true - TEST_MAX_RESOLVE_COUNT = 5 -commands = - functest-run-suite --keep-model --smoke - -[testenv:func-dev] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --dev - -[testenv:func-target] -basepython = python3 -deps = {[testenv:func-noop]deps} -commands = - functest-run-suite --keep-model --bundle {posargs} - -[coverage:run] -branch = True -concurrency = multiprocessing -parallel = True -source = - . -omit = - .tox/* - tests/* - src/templates/* - -[flake8] -ignore=E226,W504