From 8c674de50ec59c5db9207e09a7d4310baf98be3d Mon Sep 17 00:00:00 2001 From: Guillaume Boutry Date: Wed, 7 Aug 2024 17:25:48 +0200 Subject: [PATCH] [ops-sunbeam] Implement mypy linter Implement mypy in the most non-breaking way possible. There's still some changes of behavior that crept in, merely due to incorrect edge case handling. Charm libraries are generally well typed, include py.typed marker for all of the libraries, to allow mypy analyzing their usage. Change-Id: I7bda1913fa08dd4954a606526272ac80b45197cc Signed-off-by: Guillaume Boutry --- .../lib/charms/ceilometer_k8s/v0/py.typed | 0 .../lib/charms/cinder_ceph_k8s/v0/py.typed | 0 .../tests/unit/test_cinder_ceph_charm.py | 6 +- .../lib/charms/cinder_k8s/v0/py.typed | 0 .../lib/charms/designate_bind_k8s/v0/py.typed | 0 .../lib/charms/designate_k8s/v0/py.typed | 0 .../lib/charms/gnocchi_k8s/v0/py.typed | 0 .../lib/charms/keystone_k8s/v0/py.typed | 0 .../keystone_k8s/v1/identity_service.py | 159 ++++++------ .../lib/charms/keystone_k8s/v1/py.typed | 0 .../nova-k8s/lib/charms/nova_k8s/v0/py.typed | 0 .../lib/charms/ovn_central_k8s/v0/py.typed | 0 .../v0/py.typed | 0 .../lib/charms/data_platform_libs/v0/py.typed | 0 .../lib/charms/grafana_agent/v0/py.typed | 0 .../lib/charms/grafana_k8s/v0/py.typed | 0 libs/external/lib/charms/loki_k8s/v1/py.typed | 0 .../lib/charms/observability_libs/v1/py.typed | 0 .../charms/operator_libs_linux/v0/py.typed | 0 .../charms/operator_libs_linux/v2/py.typed | 0 .../lib/charms/prometheus_k8s/v0/py.typed | 0 .../lib/charms/rabbitmq_k8s/v0/py.typed | 0 .../sunbeam_nova_compute_operator/v0/py.typed | 0 .../external/lib/charms/tempo_k8s/v1/py.typed | 0 .../external/lib/charms/tempo_k8s/v2/py.typed | 0 .../tls_certificates_interface/v3/py.typed | 0 .../lib/charms/traefik_k8s/v2/py.typed | 0 .../lib/charms/traefik_route_k8s/v0/py.typed | 0 .../external/lib/charms/vault_k8s/v0/py.typed | 0 ops-sunbeam/ops_sunbeam/charm.py | 157 +++++++----- ops-sunbeam/ops_sunbeam/compound_status.py | 11 +- ops-sunbeam/ops_sunbeam/config_contexts.py | 19 +- ops-sunbeam/ops_sunbeam/container_handlers.py | 43 ++-- ops-sunbeam/ops_sunbeam/core.py | 15 +- ops-sunbeam/ops_sunbeam/guard.py | 13 +- ops-sunbeam/ops_sunbeam/interfaces.py | 35 ++- ops-sunbeam/ops_sunbeam/job_ctrl.py | 6 +- ops-sunbeam/ops_sunbeam/ovn/charm.py | 8 +- .../ops_sunbeam/ovn/container_handlers.py | 25 +- .../ops_sunbeam/ovn/relation_handlers.py | 61 +++-- ops-sunbeam/ops_sunbeam/py.typed | 0 ops-sunbeam/ops_sunbeam/relation_handlers.py | 231 ++++++++++++------ ops-sunbeam/ops_sunbeam/templating.py | 19 +- ops-sunbeam/ops_sunbeam/test_utils.py | 177 +++++++------- ops-sunbeam/ops_sunbeam/tracing.py | 2 +- run_tox.sh | 5 + tox.ini | 7 + zuul.d/project-templates.yaml | 2 + 48 files changed, 593 insertions(+), 408 deletions(-) create mode 100644 charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/py.typed create mode 100644 charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/py.typed create mode 100644 charms/cinder-k8s/lib/charms/cinder_k8s/v0/py.typed create mode 100644 charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/py.typed create mode 100644 charms/designate-k8s/lib/charms/designate_k8s/v0/py.typed create mode 100644 charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/py.typed create mode 100644 charms/keystone-k8s/lib/charms/keystone_k8s/v0/py.typed create mode 100644 charms/keystone-k8s/lib/charms/keystone_k8s/v1/py.typed create mode 100644 charms/nova-k8s/lib/charms/nova_k8s/v0/py.typed create mode 100644 charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/py.typed create mode 100644 libs/external/lib/charms/certificate_transfer_interface/v0/py.typed create mode 100644 libs/external/lib/charms/data_platform_libs/v0/py.typed create mode 100644 libs/external/lib/charms/grafana_agent/v0/py.typed create mode 100644 libs/external/lib/charms/grafana_k8s/v0/py.typed create mode 100644 libs/external/lib/charms/loki_k8s/v1/py.typed create mode 100644 libs/external/lib/charms/observability_libs/v1/py.typed create mode 100644 libs/external/lib/charms/operator_libs_linux/v0/py.typed create mode 100644 libs/external/lib/charms/operator_libs_linux/v2/py.typed create mode 100644 libs/external/lib/charms/prometheus_k8s/v0/py.typed create mode 100644 libs/external/lib/charms/rabbitmq_k8s/v0/py.typed create mode 100644 libs/external/lib/charms/sunbeam_nova_compute_operator/v0/py.typed create mode 100644 libs/external/lib/charms/tempo_k8s/v1/py.typed create mode 100644 libs/external/lib/charms/tempo_k8s/v2/py.typed create mode 100644 libs/external/lib/charms/tls_certificates_interface/v3/py.typed create mode 100644 libs/external/lib/charms/traefik_k8s/v2/py.typed create mode 100644 libs/external/lib/charms/traefik_route_k8s/v0/py.typed create mode 100644 libs/external/lib/charms/vault_k8s/v0/py.typed create mode 100644 ops-sunbeam/ops_sunbeam/py.typed diff --git a/charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/py.typed b/charms/ceilometer-k8s/lib/charms/ceilometer_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/py.typed b/charms/cinder-ceph-k8s/lib/charms/cinder_ceph_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py index 6fbc1767..6fc7d4ca 100644 --- a/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py +++ b/charms/cinder-ceph-k8s/tests/unit/test_cinder_ceph_charm.py @@ -83,8 +83,12 @@ class TestCinderCephOperatorCharm(test_utils.CharmTestCase): """Setup fixtures ready for testing.""" super().setUp(charm, self.PATCHES) self.mock_event = MagicMock() + with open("config.yaml", "r") as f: + config_data = f.read() self.harness = test_utils.get_harness( - _CinderCephOperatorCharm, container_calls=self.container_calls + _CinderCephOperatorCharm, + container_calls=self.container_calls, + charm_config=config_data, ) mock_get_platform = patch( "charmhelpers.osplatform.get_platform", return_value="ubuntu" diff --git a/charms/cinder-k8s/lib/charms/cinder_k8s/v0/py.typed b/charms/cinder-k8s/lib/charms/cinder_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/py.typed b/charms/designate-bind-k8s/lib/charms/designate_bind_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/designate-k8s/lib/charms/designate_k8s/v0/py.typed b/charms/designate-k8s/lib/charms/designate_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/py.typed b/charms/gnocchi-k8s/lib/charms/gnocchi_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v0/py.typed b/charms/keystone-k8s/lib/charms/keystone_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py b/charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py index 69e82b57..76358358 100644 --- a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py +++ b/charms/keystone-k8s/lib/charms/keystone_k8s/v1/identity_service.py @@ -79,11 +79,11 @@ import json import logging from ops.framework import ( - StoredState, EventBase, - ObjectEvents, EventSource, Object, + ObjectEvents, + StoredState, ) from ops.model import ( Relation, @@ -100,7 +100,7 @@ LIBAPI = 1 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 3 +LIBPATCH = 4 logger = logging.getLogger(__name__) @@ -140,8 +140,13 @@ class IdentityServiceRequires(Object): on = IdentityServiceServerEvents() _stored = StoredState() - def __init__(self, charm, relation_name: str, service_endpoints: dict, - region: str): + def __init__( + self, + charm, + relation_name: str, + service_endpoints: list[dict], + region: str, + ): super().__init__(charm, relation_name) self.charm = charm self.relation_name = relation_name @@ -168,9 +173,7 @@ class IdentityServiceRequires(Object): """IdentityService relation joined.""" logging.debug("IdentityService on_joined") self.on.connected.emit() - self.register_services( - self.service_endpoints, - self.region) + self.register_services(self.service_endpoints, self.region) def _on_identity_service_relation_changed(self, event): """IdentityService relation changed.""" @@ -199,92 +202,92 @@ class IdentityServiceRequires(Object): @property def api_version(self) -> str: """Return the api_version.""" - return self.get_remote_app_data('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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + 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') + credentials_id = self.get_remote_app_data("service-credentials") if not credentials_id: return None @@ -298,27 +301,27 @@ class IdentityServiceRequires(Object): @property def service_port(self) -> str: """Return the service_port.""" - return self.get_remote_app_data('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') + 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') + 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') + 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') + credentials_id = self.get_remote_app_data("service-credentials") if not credentials_id: return None @@ -332,30 +335,31 @@ class IdentityServiceRequires(Object): @property def service_user_id(self) -> str: """Return the service_user_id.""" - return self.get_remote_app_data('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') + 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') + 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') + 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') + return self.get_remote_app_data("admin-role") - def register_services(self, service_endpoints: dict, - region: str) -> None: + def register_services( + self, service_endpoints: list[dict], region: str + ) -> None: """Request access to the IdentityService server.""" if self.model.unit.is_leader(): logging.debug("Requesting service registration") @@ -375,8 +379,15 @@ class HasIdentityServiceClientsEvent(EventBase): class ReadyIdentityServiceClientsEvent(EventBase): """IdentityServiceClients Ready Event.""" - def __init__(self, handle, relation_id, relation_name, service_endpoints, - region, client_app_name): + 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 @@ -390,7 +401,8 @@ class ReadyIdentityServiceClientsEvent(EventBase): "relation_name": self.relation_name, "service_endpoints": self.service_endpoints, "client_app_name": self.client_app_name, - "region": self.region} + "region": self.region, + } def restore(self, snapshot): super().restore(snapshot) @@ -405,7 +417,9 @@ class IdentityServiceClientEvents(ObjectEvents): """Events class for `on`""" has_identity_service_clients = EventSource(HasIdentityServiceClientsEvent) - ready_identity_service_clients = EventSource(ReadyIdentityServiceClientsEvent) + ready_identity_service_clients = EventSource( + ReadyIdentityServiceClientsEvent + ) class IdentityServiceProvides(Object): @@ -441,9 +455,7 @@ class IdentityServiceProvides(Object): def _on_identity_service_relation_changed(self, event): """Handle IdentityService changed.""" logging.debug("IdentityService on_changed") - REQUIRED_KEYS = [ - 'service-endpoints', - 'region'] + REQUIRED_KEYS = ["service-endpoints", "region"] values = [ event.relation.data[event.relation.app].get(k) @@ -452,42 +464,47 @@ class IdentityServiceProvides(Object): # Validate data on the relation if all(values): service_eps = json.loads( - event.relation.data[event.relation.app]['service-endpoints']) + 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) + 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: dict, - admin_project: dict, - admin_user: dict, - service_domain: dict, - service_project: dict, - service_user: dict, - internal_auth_url: str, - admin_auth_url: str, - public_auth_url: str, - service_credentials: str, - admin_role: str): + 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: dict, + admin_project: dict, + admin_user: dict, + service_domain: dict, + service_project: dict, + service_user: dict, + 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]: diff --git a/charms/keystone-k8s/lib/charms/keystone_k8s/v1/py.typed b/charms/keystone-k8s/lib/charms/keystone_k8s/v1/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/nova-k8s/lib/charms/nova_k8s/v0/py.typed b/charms/nova-k8s/lib/charms/nova_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/py.typed b/charms/ovn-central-k8s/lib/charms/ovn_central_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/certificate_transfer_interface/v0/py.typed b/libs/external/lib/charms/certificate_transfer_interface/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/data_platform_libs/v0/py.typed b/libs/external/lib/charms/data_platform_libs/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/grafana_agent/v0/py.typed b/libs/external/lib/charms/grafana_agent/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/grafana_k8s/v0/py.typed b/libs/external/lib/charms/grafana_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/loki_k8s/v1/py.typed b/libs/external/lib/charms/loki_k8s/v1/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/observability_libs/v1/py.typed b/libs/external/lib/charms/observability_libs/v1/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/operator_libs_linux/v0/py.typed b/libs/external/lib/charms/operator_libs_linux/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/operator_libs_linux/v2/py.typed b/libs/external/lib/charms/operator_libs_linux/v2/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/prometheus_k8s/v0/py.typed b/libs/external/lib/charms/prometheus_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/rabbitmq_k8s/v0/py.typed b/libs/external/lib/charms/rabbitmq_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/sunbeam_nova_compute_operator/v0/py.typed b/libs/external/lib/charms/sunbeam_nova_compute_operator/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/tempo_k8s/v1/py.typed b/libs/external/lib/charms/tempo_k8s/v1/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/tempo_k8s/v2/py.typed b/libs/external/lib/charms/tempo_k8s/v2/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/tls_certificates_interface/v3/py.typed b/libs/external/lib/charms/tls_certificates_interface/v3/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/traefik_k8s/v2/py.typed b/libs/external/lib/charms/traefik_k8s/v2/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/traefik_route_k8s/v0/py.typed b/libs/external/lib/charms/traefik_route_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/external/lib/charms/vault_k8s/v0/py.typed b/libs/external/lib/charms/vault_k8s/v0/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/ops-sunbeam/ops_sunbeam/charm.py b/ops-sunbeam/ops_sunbeam/charm.py index c00d6285..3478f616 100644 --- a/ops-sunbeam/ops_sunbeam/charm.py +++ b/ops-sunbeam/ops_sunbeam/charm.py @@ -32,10 +32,12 @@ containers and managing the service running in the container. import ipaddress import logging import urllib +import urllib.parse from typing import ( List, Mapping, Optional, + Sequence, Set, ) @@ -52,7 +54,7 @@ import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.job_ctrl as sunbeam_job_ctrl import ops_sunbeam.relation_handlers as sunbeam_rhandlers import tenacity -from lightkube import ( +from lightkube.core.client import ( Client, ) from lightkube.resources.core_v1 import ( @@ -77,7 +79,8 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): _state = ops.framework.StoredState() # Holds set of mandatory relations - mandatory_relations = set() + mandatory_relations: set[str] = set() + service_name: str def __init__(self, framework: ops.framework.Framework) -> None: """Run constructor.""" @@ -134,8 +137,8 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): return True def get_relation_handlers( - self, handlers: List[sunbeam_rhandlers.RelationHandler] = None - ) -> List[sunbeam_rhandlers.RelationHandler]: + self, handlers: list[sunbeam_rhandlers.RelationHandler] | None = None + ) -> list[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] if self.can_add_handler("tracing", handlers): @@ -147,8 +150,8 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): self, "amqp", self.configure_charm, - self.config.get("rabbit-user") or self.service_name, - self.config.get("rabbit-vhost") or "openstack", + str(self.config.get("rabbit-user") or self.service_name), + str(self.config.get("rabbit-vhost") or "openstack"), "amqp" in self.mandatory_relations, ) handlers.append(self.amqp) @@ -220,26 +223,35 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): """Return Subject Alternate Names to use in cert for service.""" return list(set(self.get_domain_name_sans())) - def _ip_sans(self) -> List[ipaddress.IPv4Address]: - """Get IPv4 addresses for service.""" - ip_sans = [] + def _get_all_relation_addresses(self) -> list[ipaddress.IPv4Address]: + """Return all bind/ingress addresses from all relations.""" + addresses = [] for relation_name in self.meta.relations.keys(): for relation in self.framework.model.relations.get( relation_name, [] ): binding = self.model.get_binding(relation) + if binding is None or binding.network is None: + continue if isinstance( binding.network.ingress_address, ipaddress.IPv4Address ): - ip_sans.append(binding.network.ingress_address) + addresses.append(binding.network.ingress_address) if isinstance( binding.network.bind_address, ipaddress.IPv4Address ): - ip_sans.append(binding.network.bind_address) + addresses.append(binding.network.bind_address) + return addresses + + def _ip_sans(self) -> list[ipaddress.IPv4Address]: + """Get IPv4 addresses for service.""" + ip_sans = self._get_all_relation_addresses() for binding_name in ["public"]: try: binding = self.model.get_binding(binding_name) + if binding is None or binding.network is None: + continue if isinstance( binding.network.ingress_address, ipaddress.IPv4Address ): @@ -337,7 +349,7 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): @property def config_contexts( self, - ) -> List[sunbeam_config_contexts.CharmConfigContext]: + ) -> list[sunbeam_config_contexts.ConfigContext]: """Return the configuration adapters for the operator.""" return [sunbeam_config_contexts.CharmConfigContext(self, "options")] @@ -537,15 +549,22 @@ class OSBaseOperatorCharm(ops.charm.CharmBase): def bootstrapped(self) -> bool: """Determine whether the service has been bootstrapped.""" - return self._state.unit_bootstrapped and self.is_leader_ready() + return ( + self._state.unit_bootstrapped # type: ignore[truthy-function] # unit_bootstrapped is not a function + and self.is_leader_ready() + ) - def leader_set(self, settings: dict = None, **kwargs) -> None: + def leader_set( + self, + settings: sunbeam_core.RelationDataMapping | None = None, + **kwargs, + ) -> None: """Juju set data in peer data bag.""" settings = settings or {} settings.update(kwargs) self.peers.set_app_data(settings=settings) - def leader_get(self, key: str) -> str: + def leader_get(self, key: str) -> str | None: """Retrieve data from the peer relation.""" return self.peers.get_app_data(key) @@ -593,24 +612,24 @@ class OSBaseOperatorCharmK8S(OSBaseOperatorCharm): def get_named_pebble_handler( self, container_name: str - ) -> sunbeam_chandlers.PebbleHandler: + ) -> sunbeam_chandlers.PebbleHandler | None: """Get pebble handler matching container_name.""" pebble_handlers = [ h for h in self.pebble_handlers if h.container_name == container_name ] - assert len(pebble_handlers) < 2, ( - "Multiple pebble handlers with the " "same name found." - ) + assert ( + len(pebble_handlers) < 2 + ), "Multiple pebble handlers with the same name found." if pebble_handlers: return pebble_handlers[0] else: return None def get_named_pebble_handlers( - self, container_names: List[str] - ) -> List[sunbeam_chandlers.PebbleHandler]: + self, container_names: Sequence[str] + ) -> list[sunbeam_chandlers.PebbleHandler]: """Get pebble handlers matching container_names.""" return [ h @@ -644,7 +663,7 @@ class OSBaseOperatorCharmK8S(OSBaseOperatorCharm): "Container service not ready" ) - def stop_services(self, relation: Optional[Set[str]] = None) -> None: + def stop_services(self, relation: set[str] | None = None) -> None: """Stop all running services.""" for ph in self.pebble_handlers: if ph.pebble_ready: @@ -653,7 +672,7 @@ class OSBaseOperatorCharmK8S(OSBaseOperatorCharm): ) ph.stop_all() - def configure_unit(self, event: ops.framework.EventBase) -> None: + def configure_unit(self, event: ops.EventBase) -> None: """Run configuration on this unit.""" self.check_leader_ready() self.check_relation_handlers_ready(event) @@ -689,12 +708,12 @@ class OSBaseOperatorCharmK8S(OSBaseOperatorCharm): self.status.set(ActiveStatus("")) @property - def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: + def container_configs(self) -> list[sunbeam_core.ContainerConfigFile]: """Container configuration files for the operator.""" return [] @property - def container_names(self) -> List[str]: + def container_names(self) -> list[str]: """Names of Containers that form part of this service.""" return [self.service_name] @@ -746,17 +765,18 @@ class OSBaseOperatorCharmK8S(OSBaseOperatorCharm): if not self.unit.is_leader(): logging.info("Not lead unit, skipping DB syncs") return - try: - if self.db_sync_cmds: + + if db_sync_cmds := getattr(self, "db_sync_cmds", None): + if db_sync_cmds: logger.info("Syncing database...") - for cmd in self.db_sync_cmds: + for cmd in db_sync_cmds: try: self._retry_db_sync(cmd) except tenacity.RetryError: raise sunbeam_guard.BlockedExceptionError( "DB sync failed" ) - except AttributeError: + else: logger.warning( "Not DB sync ran. Charm does not specify self.db_sync_cmds" ) @@ -770,19 +790,21 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): """Base class for OpenStack API operators.""" mandatory_relations = {"database", "identity-service", "ingress-public"} + wsgi_admin_script: str + wsgi_public_script: str def __init__(self, framework: ops.framework.Framework) -> None: """Run constructor.""" super().__init__(framework) @property - def service_endpoints(self) -> List[dict]: + def service_endpoints(self) -> list[dict]: """List of endpoints for this service.""" return [] def get_relation_handlers( - self, handlers: List[sunbeam_rhandlers.RelationHandler] = None - ) -> List[sunbeam_rhandlers.RelationHandler]: + self, handlers: list[sunbeam_rhandlers.RelationHandler] | None = None + ) -> list[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] # Note: intentionally including the ingress handler here in order to @@ -813,7 +835,7 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): "identity-service", self.configure_charm, self.service_endpoints, - self.model.config["region"], + str(self.model.config["region"]), "identity-service" in self.mandatory_relations, ) handlers.append(self.id_svc) @@ -827,15 +849,11 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): call the configure_charm. """ logger.debug("Received an ingress_changed event") - try: - if self.id_svc.update_service_endpoints: - logger.debug( - "Updating service endpoints after ingress " - "relation changed." - ) - self.id_svc.update_service_endpoints(self.service_endpoints) - except (AttributeError, KeyError): - pass + if hasattr(self, "id_svc"): + logger.debug( + "Updating service endpoints after ingress " "relation changed." + ) + self.id_svc.update_service_endpoints(self.service_endpoints) self.configure_charm(event) @@ -850,7 +868,7 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): charm_service = client.get( Service, name=self.app.name, namespace=self.model.name ) - + public_address = None status = charm_service.status if status: load_balancer_status = status.loadBalancer @@ -867,12 +885,21 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): "Using ingress address from loadbalancer " f"as {addr}" ) - return ingress_address.hostname or ingress_address.ip + public_address = ( + ingress_address.hostname or ingress_address.ip + ) - hostname = self.model.get_binding( - "identity-service" - ).network.ingress_address - return hostname + if not public_address: + binding = self.model.get_binding("identity-service") + if binding and binding.network and binding.network.ingress_address: + public_address = str(binding.network.ingress_address) + + if not public_address: + raise sunbeam_guard.WaitingExceptionError( + "No public address found for service" + ) + + return public_address @property def public_url(self) -> str: @@ -895,15 +922,19 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): @property def admin_url(self) -> str: """Url for accessing the admin endpoint for this service.""" - hostname = self.model.get_binding( - "identity-service" - ).network.ingress_address - return self.add_explicit_port(self.service_url(hostname)) + binding = self.model.get_binding("identity-service") + if binding and binding.network and binding.network.ingress_address: + return self.add_explicit_port( + self.service_url(str(binding.network.ingress_address)) + ) + raise sunbeam_guard.WaitingExceptionError( + "No admin address found for service" + ) @property def internal_url(self) -> str: """Url for accessing the internal endpoint for this service.""" - try: + if hasattr(self, "ingress_internal"): if self.ingress_internal.url: logger.debug( "Ingress-internal relation found, returning " @@ -911,15 +942,17 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): self.ingress_internal.url, ) return self.add_explicit_port(self.ingress_internal.url) - except (AttributeError, KeyError): - pass - hostname = self.model.get_binding( - "identity-service" - ).network.ingress_address - return self.add_explicit_port(self.service_url(hostname)) + binding = self.model.get_binding("identity-service") + if binding and binding.network and binding.network.ingress_address: + return self.add_explicit_port( + self.service_url(str(binding.network.ingress_address)) + ) + raise sunbeam_guard.WaitingExceptionError( + "No internal address found for service" + ) - def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]: + def get_pebble_handlers(self) -> list[sunbeam_chandlers.PebbleHandler]: """Pebble handlers for the service.""" return [ sunbeam_chandlers.WSGIPebbleHandler( @@ -934,7 +967,7 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): ] @property - def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]: + def container_configs(self) -> list[sunbeam_core.ContainerConfigFile]: """Container configuration files for the service.""" _cconfigs = super().container_configs _cconfigs.extend( @@ -964,7 +997,7 @@ class OSBaseOperatorAPICharm(OSBaseOperatorCharmK8S): return f"/etc/{self.service_name}/{self.service_name}.conf" @property - def config_contexts(self) -> List[sunbeam_config_contexts.ConfigContext]: + def config_contexts(self) -> list[sunbeam_config_contexts.ConfigContext]: """Generate list of configuration adapters for the charm.""" _cadapters = super().config_contexts _cadapters.extend( diff --git a/ops-sunbeam/ops_sunbeam/compound_status.py b/ops-sunbeam/ops_sunbeam/compound_status.py index f7d0624f..79ebb639 100644 --- a/ops-sunbeam/ops_sunbeam/compound_status.py +++ b/ops-sunbeam/ops_sunbeam/compound_status.py @@ -24,6 +24,7 @@ aspects of the application without clobbering other parts. """ import json import logging +import typing from typing import ( Callable, Dict, @@ -157,12 +158,14 @@ class StatusPool(Object): ) try: - self._state = charm.framework.load_snapshot(stored_handle) - status_state = json.loads(self._state["statuses"]) + self._state = typing.cast( + StoredStateData, charm.framework.load_snapshot(stored_handle) + ) + status_state: dict = json.loads(self._state["statuses"]) except NoSnapshotError: self._state = StoredStateData(self, "_status_pool") - status_state = [] - self._status_state = status_state + status_state = {} + self._status_state: dict = status_state # 'commit' is an ops framework event # that tells the object to save a snapshot of its state for later. diff --git a/ops-sunbeam/ops_sunbeam/config_contexts.py b/ops-sunbeam/ops_sunbeam/config_contexts.py index 655e1bf3..ef5afc4f 100644 --- a/ops-sunbeam/ops_sunbeam/config_contexts.py +++ b/ops-sunbeam/ops_sunbeam/config_contexts.py @@ -19,16 +19,15 @@ create reusable contexts which translate charm config, deployment state etc. These are not specific to a relation. """ -from __future__ import ( - annotations, -) - import logging from typing import ( TYPE_CHECKING, ) import ops_sunbeam.tracing as sunbeam_tracing +from ops_sunbeam.core import ( + ContextMapping, +) if TYPE_CHECKING: import ops_sunbeam.charm @@ -61,7 +60,7 @@ class ConfigContext: """Whether the context has all the data is needs.""" return True - def context(self) -> dict: + def context(self) -> ContextMapping: """Context used when rendering templates.""" raise NotImplementedError @@ -70,7 +69,7 @@ class ConfigContext: class CharmConfigContext(ConfigContext): """A context containing all of the charms config options.""" - def context(self) -> dict: + def context(self) -> ContextMapping: """Charms config options.""" return self.charm.config @@ -79,7 +78,9 @@ class CharmConfigContext(ConfigContext): class WSGIWorkerConfigContext(ConfigContext): """Configuration context for WSGI configuration.""" - def context(self) -> dict: + charm: "ops_sunbeam.charm.OSBaseOperatorAPICharm" + + def context(self) -> ContextMapping: """WSGI configuration options.""" return { "name": self.charm.service_name, @@ -97,7 +98,7 @@ class WSGIWorkerConfigContext(ConfigContext): class CephConfigurationContext(ConfigContext): """Ceph configuration context.""" - def context(self) -> None: + def context(self) -> ContextMapping: """Ceph configuration context.""" config = self.charm.model.config.get ctxt = {} @@ -113,7 +114,7 @@ class CephConfigurationContext(ConfigContext): class CinderCephConfigurationContext(ConfigContext): """Cinder Ceph configuration context.""" - def context(self) -> None: + def context(self) -> ContextMapping: """Cinder Ceph configuration context.""" config = self.charm.model.config.get data_pool_name = config("rbd-pool-name") or self.charm.app.name diff --git a/ops-sunbeam/ops_sunbeam/container_handlers.py b/ops-sunbeam/ops_sunbeam/container_handlers.py index 340e7319..3a1e0152 100644 --- a/ops-sunbeam/ops_sunbeam/container_handlers.py +++ b/ops-sunbeam/ops_sunbeam/container_handlers.py @@ -21,13 +21,10 @@ in the container. import collections import logging +import typing from collections.abc import ( Callable, ) -from typing import ( - List, - TypedDict, -) import ops.charm import ops.pebble @@ -41,6 +38,12 @@ from ops.model import ( WaitingStatus, ) +if typing.TYPE_CHECKING: + from ops_sunbeam.charm import ( + OSBaseOperatorAPICharm, + OSBaseOperatorCharm, + ) + logger = logging.getLogger(__name__) ContainerDir = collections.namedtuple( @@ -54,10 +57,10 @@ class PebbleHandler(ops.framework.Object): def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", container_name: str, service_name: str, - container_configs: List[sunbeam_core.ContainerConfigFile], + container_configs: list[sunbeam_core.ContainerConfigFile], template_dir: str, callback_f: Callable, ) -> None: @@ -129,16 +132,16 @@ class PebbleHandler(ops.framework.Object): logger.debug("No file changes detected") return files_updated - def get_layer(self) -> dict: + def get_layer(self) -> ops.pebble.LayerDict: """Pebble configuration layer for the container.""" return {} - def get_healthcheck_layer(self) -> dict: + def get_healthcheck_layer(self) -> ops.pebble.LayerDict: """Pebble configuration for health check layer for the container.""" return {} @property - def directories(self) -> List[ContainerDir]: + def directories(self) -> list[ContainerDir]: """List of directories to create in container.""" return [] @@ -165,7 +168,7 @@ class PebbleHandler(ops.framework.Object): def default_container_configs( self, - ) -> List[sunbeam_core.ContainerConfigFile]: + ) -> list[sunbeam_core.ContainerConfigFile]: """Generate default container configurations. These should be used by all inheriting classes and are @@ -189,7 +192,7 @@ class PebbleHandler(ops.framework.Object): return all([s.is_running() for s in services.values()]) def execute( - self, cmd: List, exception_on_error: bool = False, **kwargs: TypedDict + self, cmd: list[str], exception_on_error: bool = False, **kwargs ) -> str: """Execute given command in container managed by this handler. @@ -214,10 +217,12 @@ class PebbleHandler(ops.framework.Object): return stdout except ops.pebble.ExecError as e: logger.error("Exited with code %d. Stderr:", e.exit_code) - for line in e.stderr.splitlines(): - logger.error(" %s", line) + if e.stderr: + for line in e.stderr.splitlines(): + logger.error(" %s", line) if exception_on_error: raise + return "" def add_healthchecks(self) -> None: """Add healthcheck layer to the plan.""" @@ -363,12 +368,14 @@ class ServicePebbleHandler(PebbleHandler): class WSGIPebbleHandler(PebbleHandler): """WSGI oriented handler for a Pebble managed container.""" + charm: "OSBaseOperatorAPICharm" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorAPICharm", container_name: str, service_name: str, - container_configs: List[sunbeam_core.ContainerConfigFile], + container_configs: list[sunbeam_core.ContainerConfigFile], template_dir: str, callback_f: Callable, wsgi_service_name: str, @@ -406,7 +413,7 @@ class WSGIPebbleHandler(PebbleHandler): """Start the service.""" self.start_wsgi() - def get_layer(self) -> dict: + def get_layer(self) -> ops.pebble.LayerDict: """Apache WSGI service pebble layer. :returns: pebble layer configuration for wsgi service @@ -424,7 +431,7 @@ class WSGIPebbleHandler(PebbleHandler): }, } - def get_healthcheck_layer(self) -> dict: + def get_healthcheck_layer(self) -> ops.pebble.LayerDict: """Apache WSGI health check pebble layer. :returns: pebble health check layer configuration for wsgi service @@ -483,7 +490,7 @@ class WSGIPebbleHandler(PebbleHandler): def default_container_configs( self, - ) -> List[sunbeam_core.ContainerConfigFile]: + ) -> list[sunbeam_core.ContainerConfigFile]: """Container configs for WSGI service.""" return [ sunbeam_core.ContainerConfigFile(self.wsgi_conf, "root", "root") diff --git a/ops-sunbeam/ops_sunbeam/core.py b/ops-sunbeam/ops_sunbeam/core.py index d242ac54..313152e7 100644 --- a/ops-sunbeam/ops_sunbeam/core.py +++ b/ops-sunbeam/ops_sunbeam/core.py @@ -18,8 +18,9 @@ import collections from typing import ( TYPE_CHECKING, Generator, - List, - Tuple, + Mapping, + MutableMapping, + Sequence, Union, ) @@ -42,6 +43,10 @@ ContainerConfigFile = collections.namedtuple( defaults=(None,), ) +RelationDataMapping = MutableMapping[str, str] +ConfigMapping = Mapping[str, bool | int | float | str | None] +ContextMapping = RelationDataMapping | ConfigMapping + @sunbeam_tracing.trace_type class OPSCharmContexts: @@ -50,7 +55,7 @@ class OPSCharmContexts: def __init__(self, charm: "OSBaseOperatorCharm") -> None: """Run constructor.""" self.charm = charm - self.namespaces = [] + self.namespaces: list[str] = [] def add_relation_handler(self, handler: "RelationHandler") -> None: """Add relation handler.""" @@ -67,7 +72,7 @@ class OPSCharmContexts: setattr(self, "leader_db", obj) def add_config_contexts( - self, config_adapters: List["ConfigContext"] + self, config_adapters: Sequence["ConfigContext"] ) -> None: """Add multiple config contexts.""" for config_adapter in config_adapters: @@ -83,7 +88,7 @@ class OPSCharmContexts: def __iter__( self, ) -> Generator[ - Tuple[str, Union["ConfigContext", "RelationHandler"]], None, None + tuple[str, Union["ConfigContext", "RelationHandler"]], None, None ]: """Iterate over the relations presented to the charm.""" for namespace in self.namespaces: diff --git a/ops-sunbeam/ops_sunbeam/guard.py b/ops-sunbeam/ops_sunbeam/guard.py index 581d4d26..331d3057 100644 --- a/ops-sunbeam/ops_sunbeam/guard.py +++ b/ops-sunbeam/ops_sunbeam/guard.py @@ -15,19 +15,22 @@ """Module to handle errors and bailing out of an event/hook.""" import logging +import typing from contextlib import ( contextmanager, ) -from ops.charm import ( - CharmBase, -) from ops.model import ( BlockedStatus, MaintenanceStatus, WaitingStatus, ) +if typing.TYPE_CHECKING: + from ops_sunbeam.charm import ( + OSBaseOperatorCharm, + ) + logger = logging.getLogger(__name__) @@ -65,12 +68,12 @@ class WaitingExceptionError(BaseStatusExceptionError): @contextmanager def guard( - charm: "CharmBase", + charm: "OSBaseOperatorCharm", section: str, handle_exception: bool = True, log_traceback: bool = True, **__, -) -> None: +) -> typing.Generator: """Context manager to handle errors and bailing out of an event/hook. The nature of Juju is that all the information may not be available to run diff --git a/ops-sunbeam/ops_sunbeam/interfaces.py b/ops-sunbeam/ops_sunbeam/interfaces.py index c86ba13b..479c5f0a 100644 --- a/ops-sunbeam/ops_sunbeam/interfaces.py +++ b/ops-sunbeam/ops_sunbeam/interfaces.py @@ -15,11 +15,7 @@ """Common interfaces not charm specific.""" import logging -from typing import ( - Dict, - List, - Optional, -) +import typing import ops.model from ops.framework import ( @@ -29,6 +25,9 @@ from ops.framework import ( ObjectEvents, StoredState, ) +from ops_sunbeam.core import ( + RelationDataMapping, +) class PeersRelationCreatedEvent(EventBase): @@ -66,7 +65,7 @@ class PeersEvents(ObjectEvents): class OperatorPeers(Object): """Interface for the peers relation.""" - on = PeersEvents() + on = PeersEvents() # type: ignore state = StoredState() def __init__(self, charm: ops.charm.CharmBase, relation_name: str) -> None: @@ -89,50 +88,50 @@ class OperatorPeers(Object): return self.framework.model.get_relation(self.relation_name) @property - def _app_data_bag(self) -> Dict[str, str]: + def _app_data_bag(self) -> typing.MutableMapping[str, str]: """Return all app data on peer relation.""" if not self.peers_rel: return {} return self.peers_rel.data[self.peers_rel.app] - def on_joined(self, event: ops.framework.EventBase) -> None: + def on_joined(self, event: ops.EventBase) -> None: """Handle relation joined event.""" logging.info("Peer joined") self.on.peers_relation_joined.emit() - def on_created(self, event: ops.framework.EventBase) -> None: + def on_created(self, event: ops.EventBase) -> None: """Handle relation created event.""" logging.info("Peers on_created") self.on.peers_relation_created.emit() - def on_changed(self, event: ops.framework.EventBase) -> None: + def on_changed(self, event: ops.EventBase) -> None: """Handle relation changed event.""" logging.info("Peers on_changed") self.on.peers_data_changed.emit() - def set_app_data(self, settings: Dict[str, str]) -> None: + def set_app_data(self, settings: RelationDataMapping) -> None: """Publish settings on the peer app data bag.""" for k, v in settings.items(): self._app_data_bag[k] = v - def get_app_data(self, key: str) -> Optional[str]: + def get_app_data(self, key: str) -> str | None: """Get the value corresponding to key from the app data bag.""" if not self.peers_rel: return None return self._app_data_bag.get(key) - def get_all_app_data(self) -> Dict[str, str]: + def get_all_app_data(self) -> typing.MutableMapping[str, str]: """Return all the app data from the relation.""" return self._app_data_bag def get_all_unit_values( self, key: str, include_local_unit: bool = False - ) -> List[str]: + ) -> list[str]: """Retrieve value for key from all related units. :param include_local_unit: Include value set by local unit """ - values = [] + values: list[str] = [] if not self.peers_rel: return values for unit in self.peers_rel.units: @@ -144,17 +143,17 @@ class OperatorPeers(Object): values.append(local_unit_value) return values - def set_unit_data(self, settings: Dict[str, str]) -> None: + def set_unit_data(self, settings: typing.Mapping[str, str]) -> None: """Publish settings on the peer unit data bag.""" if not self.peers_rel: return for k, v in settings.items(): self.peers_rel.data[self.model.unit][k] = v - def all_joined_units(self) -> List[ops.model.Unit]: + def all_joined_units(self) -> set[ops.model.Unit]: """All remote units joined to the peer relation.""" if not self.peers_rel: - return [] + return set() return set(self.peers_rel.units) def expected_peer_units(self) -> int: diff --git a/ops-sunbeam/ops_sunbeam/job_ctrl.py b/ops-sunbeam/ops_sunbeam/job_ctrl.py index 61f843ca..9e07917b 100644 --- a/ops-sunbeam/ops_sunbeam/job_ctrl.py +++ b/ops-sunbeam/ops_sunbeam/job_ctrl.py @@ -20,16 +20,14 @@ case these helpers can limit how frequently they are run. import logging import time +import typing from functools import ( wraps, ) -from typing import ( - TYPE_CHECKING, -) import ops.framework -if TYPE_CHECKING: +if typing.TYPE_CHECKING: import ops_sunbeam.charm logger = logging.getLogger(__name__) diff --git a/ops-sunbeam/ops_sunbeam/ovn/charm.py b/ops-sunbeam/ops_sunbeam/ovn/charm.py index 3d69ab9b..f6fbcaf4 100644 --- a/ops-sunbeam/ops_sunbeam/ovn/charm.py +++ b/ops-sunbeam/ops_sunbeam/ovn/charm.py @@ -14,10 +14,6 @@ """Base classes for defining an OVN charm using the Operator framework.""" -from typing import ( - List, -) - from .. import charm as sunbeam_charm from .. import relation_handlers as sunbeam_rhandlers from . import relation_handlers as ovn_relation_handlers @@ -27,8 +23,8 @@ class OSBaseOVNOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): """Base charms for OpenStack operators.""" def get_relation_handlers( - self, handlers: List[sunbeam_rhandlers.RelationHandler] = None - ) -> List[sunbeam_rhandlers.RelationHandler]: + self, handlers: list[sunbeam_rhandlers.RelationHandler] | None = None + ) -> list[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] if self.can_add_handler("ovsdb-cms", handlers): diff --git a/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py b/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py index 00f92421..810e4ef8 100644 --- a/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py +++ b/ops-sunbeam/ops_sunbeam/ovn/container_handlers.py @@ -14,17 +14,10 @@ """Base classes for defining OVN Pebble handlers.""" -from typing import ( - List, -) - -from ops.model import ( - ActiveStatus, -) - -from .. import container_handlers as sunbeam_chandlers -from .. import core as sunbeam_core -from .. import tracing as sunbeam_tracing +import ops +import ops_sunbeam.container_handlers as sunbeam_chandlers +import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.tracing as sunbeam_tracing @sunbeam_tracing.trace_type @@ -52,14 +45,14 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): self.setup_dirs() changes = self.write_config(context) self.files_changed(changes) - self.status.set(ActiveStatus("")) + self.status.set(ops.ActiveStatus("")) @property def service_description(self) -> str: """Return a short description of service e.g. OVN Southbound DB.""" raise NotImplementedError - def get_layer(self) -> dict: + def get_layer(self) -> ops.pebble.LayerDict: """Pebble configuration layer for OVN service. :returns: pebble layer configuration for service @@ -80,7 +73,7 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): }, } - def get_healthcheck_layer(self) -> dict: + def get_healthcheck_layer(self) -> ops.pebble.LayerDict: """Health check pebble layer. :returns: pebble health check layer configuration for OVN service @@ -97,7 +90,7 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): } @property - def directories(self) -> List[sunbeam_chandlers.ContainerDir]: + def directories(self) -> list[sunbeam_chandlers.ContainerDir]: """Directories to creete in container.""" return [ sunbeam_chandlers.ContainerDir("/etc/ovn", "root", "root"), @@ -108,7 +101,7 @@ class OVNPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): def default_container_configs( self, - ) -> List[sunbeam_core.ContainerConfigFile]: + ) -> list[sunbeam_core.ContainerConfigFile]: """Files to render into containers.""" return [ sunbeam_core.ContainerConfigFile( diff --git a/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py b/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py index 409b9e34..8341c201 100644 --- a/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py +++ b/ops-sunbeam/ops_sunbeam/ovn/relation_handlers.py @@ -18,29 +18,44 @@ import ipaddress import itertools import logging import socket +import typing from typing import ( Callable, - Dict, Iterator, - List, ) import ops.charm import ops.framework +import ops_sunbeam.interfaces as sunbeam_interfaces +import ops_sunbeam.relation_handlers as sunbeam_rhandlers +import ops_sunbeam.tracing as sunbeam_tracing from ops.model import ( BlockedStatus, ) +from ops_sunbeam.charm import ( + OSBaseOperatorCharm, +) -from .. import relation_handlers as sunbeam_rhandlers -from .. import tracing as sunbeam_tracing +if typing.TYPE_CHECKING: + import charms.ovn_central_k8s.v0.ovsdb as ovsdb logger = logging.getLogger(__name__) +IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address | str + @sunbeam_tracing.trace_type class OVNRelationUtils: """Common utilities for processing OVN relations.""" + charm: OSBaseOperatorCharm + relation_name: str + interface: typing.Union[ + "ovsdb.OVSDBCMSRequires", + "ovsdb.OVSDBCMSProvides", + sunbeam_interfaces.OperatorPeers, + ] + DB_NB_PORT = 6641 DB_SB_PORT = 6642 DB_SB_ADMIN_PORT = 16642 @@ -71,7 +86,7 @@ class OVNRelationUtils: :returns: addresses published by remote units. :rtype: Iterator[str] """ - for addr in self.interface.get_all_unit_values(key): + for addr in self.interface.get_all_unit_values(key): # type: ignore try: addr = self._format_addr(addr) yield addr @@ -86,7 +101,7 @@ class OVNRelationUtils: :returns: hostnames published by remote units. :rtype: Iterator[str] """ - for hostname in self.interface.get_all_unit_values(key): + for hostname in self.interface.get_all_unit_values(key): # type: ignore yield hostname @property @@ -117,7 +132,7 @@ class OVNRelationUtils: return self._remote_addrs("ingress-bound-address") def db_connection_strs( - self, hostnames: List[str], port: int, proto: str = "ssl" + self, hostnames: typing.Iterable[str], port: int, proto: str = "ssl" ) -> Iterator[str]: """Provide connection strings. @@ -249,7 +264,7 @@ class OVNRelationUtils: ) @property - def cluster_local_addr(self) -> ipaddress.IPv4Address: + def cluster_local_addr(self) -> IPAddress | None: """Retrieve local address bound to endpoint. :returns: IPv4 or IPv6 address bound to endpoint @@ -258,7 +273,7 @@ class OVNRelationUtils: return self._endpoint_local_bound_addr() @property - def cluster_ingress_addr(self) -> ipaddress.IPv4Address: + def cluster_ingress_addr(self) -> IPAddress | None: """Retrieve local address bound to endpoint. :returns: IPv4 or IPv6 address bound to endpoint @@ -284,7 +299,7 @@ class OVNRelationUtils: """ return socket.getfqdn() - def _endpoint_local_bound_addr(self) -> ipaddress.IPv4Address: + def _endpoint_local_bound_addr(self) -> IPAddress | None: """Retrieve local address bound to endpoint. :returns: IPv4 or IPv6 address bound to endpoint @@ -292,19 +307,25 @@ class OVNRelationUtils: addr = None for relation in self.charm.model.relations.get(self.relation_name, []): binding = self.charm.model.get_binding(relation) - addr = binding.network.bind_address + if binding and binding.network and binding.network.bind_address: + addr = binding.network.bind_address break return addr - def _endpoint_ingress_bound_addresses(self) -> ipaddress.IPv4Address: + def _endpoint_ingress_bound_addresses(self) -> list[IPAddress]: """Retrieve local address bound to endpoint. :returns: IPv4 or IPv6 address bound to endpoint """ - addresses = [] + addresses: list[IPAddress] = [] for relation in self.charm.model.relations.get(self.relation_name, []): binding = self.charm.model.get_binding(relation) - addresses.extend(binding.network.ingress_addresses) + if ( + binding + and binding.network + and binding.network.ingress_addresses + ): + addresses.extend(binding.network.ingress_addresses) return list(set(addresses)) @@ -314,7 +335,9 @@ class OVNDBClusterPeerHandler( ): """Handle OVN peer relation.""" - def publish_cluster_local_hostname(self, hostname: str = None) -> Dict: + interface: sunbeam_interfaces.OperatorPeers + + def publish_cluster_local_hostname(self, hostname: str | None = None): """Announce hostname on relation. This will be used by our peers and clients to build a connection @@ -467,9 +490,11 @@ class OVSDBCMSProvidesHandler( ): """Handle provides side of ovsdb-cms.""" + interface: "ovsdb.OVSDBCMSProvides" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -520,9 +545,11 @@ class OVSDBCMSRequiresHandler( ): """Handle provides side of ovsdb-cms.""" + interface: "ovsdb.OVSDBCMSRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, diff --git a/ops-sunbeam/ops_sunbeam/py.typed b/ops-sunbeam/ops_sunbeam/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/ops-sunbeam/ops_sunbeam/relation_handlers.py b/ops-sunbeam/ops_sunbeam/relation_handlers.py index e54c5de7..614c6523 100644 --- a/ops-sunbeam/ops_sunbeam/relation_handlers.py +++ b/ops-sunbeam/ops_sunbeam/relation_handlers.py @@ -23,12 +23,6 @@ import string import typing from typing import ( Callable, - Dict, - FrozenSet, - List, - Optional, - Tuple, - Union, ) from urllib.parse import ( urlparse, @@ -47,6 +41,30 @@ from ops.model import ( UnknownStatus, WaitingStatus, ) +from ops_sunbeam.core import ( + RelationDataMapping, +) + +if typing.TYPE_CHECKING: + import charms.ceilometer_k8s.v0.ceilometer_service as ceilometer_service + import charms.certificate_transfer_interface.v0.certificate_transfer as certificate_transfer + import charms.cinder_ceph_k8s.v0.ceph_access as ceph_access + import charms.data_platform_libs.v0.data_interfaces as data_interfaces + import charms.keystone_k8s.v0.identity_credentials as identity_credentials + import charms.keystone_k8s.v0.identity_resource as identity_resource + import charms.keystone_k8s.v1.identity_service as identity_service + import charms.loki_k8s.v1.loki_push_api as loki_push_api + import charms.nova_k8s.v0.nova_service as nova_service + import charms.rabbitmq_k8s.v0.rabbitmq as rabbitmq + import charms.tempo_k8s.v2.tracing as tracing + import charms.tls_certificates_interface.v3.tls_certificates as tls_certificates + import charms.traefik_k8s.v2.ingress as ingress + import charms.traefik_route_k8s.v0.traefik_route as traefik_route + import interface_ceph_client.ceph_client as ceph_client # type: ignore [import-untyped] + from ops_sunbeam.charm import ( + OSBaseOperatorCharm, + ) + logger = logging.getLogger(__name__) @@ -71,7 +89,7 @@ class RelationHandler(ops.framework.Object): def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -109,7 +127,7 @@ class RelationHandler(ops.framework.Object): else: status.set(WaitingStatus("integration incomplete")) - def setup_event_handler(self) -> ops.framework.Object: + def setup_event_handler(self) -> ops.Object: """Configure event handlers for the relation. This method must be overridden in concrete class @@ -117,7 +135,7 @@ class RelationHandler(ops.framework.Object): """ raise NotImplementedError - def get_interface(self) -> Tuple[ops.framework.Object, str]: + def get_interface(self) -> tuple[ops.Object, str]: """Return the interface that this handler encapsulates. This is a combination of the interface object and the @@ -157,9 +175,11 @@ class RelationHandler(ops.framework.Object): class IngressHandler(RelationHandler): """Base class to handle Ingress relations.""" + interface: "ingress.IngressPerAppRequirer" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, service_name: str, default_ingress_port: int, @@ -235,7 +255,7 @@ class IngressHandler(RelationHandler): return False @property - def url(self) -> Optional[str]: + def url(self) -> str | None: """Return the URL used by the remote ingress service.""" if not self.ready: return None @@ -264,9 +284,11 @@ class IngressPublicHandler(IngressHandler): class DBHandler(RelationHandler): """Handler for DB relations.""" + interface: "data_interfaces.DatabaseRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, database: str, @@ -323,7 +345,14 @@ class DBHandler(RelationHandler): # this will be set to self.interface in parent class return db - def _on_database_updated(self, event: ops.framework.EventBase) -> None: + def _on_database_updated( + self, + event: typing.Union[ + "data_interfaces.DatabaseCreatedEvent", + "data_interfaces.DatabaseEndpointsChangedEvent", + "data_interfaces.DatabaseReadOnlyEndpointsChangedEvent", + ], + ) -> None: """Handle database change events.""" if not (event.username or event.password or event.endpoints): return @@ -342,7 +371,7 @@ class DBHandler(RelationHandler): if self.mandatory: self.status.set(BlockedStatus("integration missing")) - def get_relation_data(self) -> dict: + def get_relation_data(self) -> RelationDataMapping: """Load the data from the relation for consumption in the handler.""" # there is at most one relation for a database for relation in self.model.relations[self.relation_name]: @@ -400,15 +429,16 @@ class DBHandler(RelationHandler): class RabbitMQHandler(RelationHandler): """Handler for managing a rabbitmq relation.""" + interface: "rabbitmq.RabbitMQRequires" DEFAULT_PORT = "5672" def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, username: str, - vhost: int, + vhost: str, mandatory: bool = False, ) -> None: """Run constructor.""" @@ -495,12 +525,14 @@ class AMQPHandler(RabbitMQHandler): class IdentityServiceRequiresHandler(RelationHandler): """Handler for managing a identity-service relation.""" + interface: "identity_service.IdentityServiceRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, - service_endpoints: dict, + service_endpoints: list[dict], region: str, mandatory: bool = False, ) -> None: @@ -543,7 +575,7 @@ class IdentityServiceRequiresHandler(RelationHandler): if self.mandatory: self.status.set(BlockedStatus("integration missing")) - def update_service_endpoints(self, service_endpoints: dict) -> None: + def update_service_endpoints(self, service_endpoints: list[dict]) -> None: """Update service endpoints on the relation.""" self.service_endpoints = service_endpoints self.interface.register_services(service_endpoints, self.region) @@ -561,9 +593,10 @@ class IdentityServiceRequiresHandler(RelationHandler): class BasePeerHandler(RelationHandler): """Base handler for managing a peers relation.""" + interface: sunbeam_interfaces.OperatorPeers LEADER_READY_KEY = "leader_ready" - def setup_event_handler(self) -> None: + def setup_event_handler(self) -> ops.Object: """Configure event handlers for peer relation.""" logger.debug("Setting up peer event handler") # Lazy import to ensure this lib is only required if the charm @@ -609,19 +642,21 @@ class BasePeerHandler(RelationHandler): except (AttributeError, KeyError): return {} - def set_app_data(self, settings: dict) -> None: + def set_app_data(self, settings: RelationDataMapping) -> None: """Store data in peer app db.""" self.interface.set_app_data(settings) - def get_app_data(self, key: str) -> Optional[str]: + def get_app_data(self, key: str) -> str | None: """Retrieve data from the peer relation.""" return self.interface.get_app_data(key) - def leader_get(self, key: str) -> str: + def leader_get(self, key: str) -> str | None: """Retrieve data from the peer relation.""" - return self.peers.get_app_data(key) + return self.interface.get_app_data(key) - def leader_set(self, settings: dict, **kwargs) -> None: + def leader_set( + self, settings: RelationDataMapping | None, **kwargs + ) -> None: """Store data in peer app db.""" settings = settings or {} settings.update(kwargs) @@ -639,13 +674,13 @@ class BasePeerHandler(RelationHandler): else: return json.loads(ready) - def set_unit_data(self, settings: Dict[str, str]) -> None: + def set_unit_data(self, settings: dict[str, str]) -> None: """Publish settings on the peer unit data bag.""" self.interface.set_unit_data(settings) def get_all_unit_values( self, key: str, include_local_unit: bool = False - ) -> List[str]: + ) -> list[str]: """Retrieve value for key from all related units. :param include_local_unit: Include value set by local unit @@ -659,13 +694,15 @@ class BasePeerHandler(RelationHandler): class CephClientHandler(RelationHandler): """Handler for ceph-client interface.""" + interface: "ceph_client.CephClientRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, allow_ec_overwrites: bool = True, - app_name: str = None, + app_name: str | None = None, mandatory: bool = False, ) -> None: """Run constructor.""" @@ -713,15 +750,22 @@ class CephClientHandler(RelationHandler): or config("rbd-pool") or self.charm.app.name ) - metadata_pool_name = ( - config("ec-rbd-metadata-pool") or f"{self.charm.app.name}-metadata" + # schema defined as str + metadata_pool_name: str = typing.cast( + str, + config("ec-rbd-metadata-pool") + or f"{self.charm.app.name}-metadata", ) - weight = config("ceph-pool-weight") - replicas = config("ceph-osd-replication-count") + # schema defined as int and with a default + # weight is then managed as a float. + weight = float(typing.cast(int, config("ceph-pool-weight"))) + # schema defined as int and with a default + replicas = typing.cast(int, config("ceph-osd-replication-count")) # TODO: add bluestore compression options if config("pool-type") == ERASURE_CODED: # General EC plugin config - plugin = config("ec-profile-plugin") + # schema defined as str and with a default + plugin = typing.cast(str, config("ec-profile-plugin")) technique = config("ec-profile-technique") device_class = config("ec-profile-device-class") bdm_k = config("ec-profile-k") @@ -788,7 +832,7 @@ class CephClientHandler(RelationHandler): return self.interface.pools_available @property - def key(self) -> str: + def key(self) -> str | None: """Retrieve the cephx key provided for the application.""" return self.interface.get_relation_data().get("key") @@ -796,7 +840,11 @@ class CephClientHandler(RelationHandler): """Context containing Ceph connection data.""" ctxt = super().context() data = self.interface.get_relation_data() - ctxt["mon_hosts"] = ",".join(sorted(data.get("mon_hosts"))) + # mon_hosts is a list of sorted host strings + mon_hosts = typing.cast(list[str] | None, data.get("mon_hosts")) + if not mon_hosts: + return {} + ctxt["mon_hosts"] = ",".join(mon_hosts) ctxt["auth"] = data.get("auth") ctxt["key"] = data.get("key") ctxt["rbd_features"] = None @@ -878,12 +926,8 @@ class _Store(abc.ABC): class TlsCertificatesHandler(RelationHandler): """Handler for certificates interface.""" - if typing.TYPE_CHECKING: - from charms.tls_certificates_interface.v3.tls_certificates import ( - TLSCertificatesRequiresV3, - ) - - interface: TLSCertificatesRequiresV3 + interface: "tls_certificates.TLSCertificatesRequiresV3" + store: _Store class PeerStore(_Store): """Store private key secret id in peer storage relation.""" @@ -943,7 +987,7 @@ class TlsCertificatesHandler(RelationHandler): def __init__( self, - charm: ops.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, sans_dns: list[str] | None = None, @@ -956,9 +1000,9 @@ class TlsCertificatesHandler(RelationHandler): self.sans_ips = sans_ips super().__init__(charm, relation_name, callback_f, mandatory) try: - self.store = self.PeerStore( - self.model.get_relation("peers"), self.get_entity() - ) + peer_relation = self.model.get_relation("peers") + # TODO(gboutry): fix type ignore + self.store = self.PeerStore(peer_relation, self.get_entity()) # type: ignore[arg-type] except KeyError: if self.app_managed_certificates(): raise RuntimeError( @@ -1269,9 +1313,11 @@ class TlsCertificatesHandler(RelationHandler): class IdentityCredentialsRequiresHandler(RelationHandler): """Handles the identity credentials relation on the requires side.""" + interface: "identity_credentials.IdentityCredentialsRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -1333,9 +1379,11 @@ class IdentityCredentialsRequiresHandler(RelationHandler): class IdentityResourceRequiresHandler(RelationHandler): """Handles the identity resource relation on the requires side.""" + interface: "identity_resource.IdentityResourceRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -1410,9 +1458,11 @@ class IdentityResourceRequiresHandler(RelationHandler): class CeilometerServiceRequiresHandler(RelationHandler): """Handle ceilometer service relation on the requires side.""" + interface: "ceilometer_service.CeilometerServiceRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -1435,7 +1485,7 @@ class CeilometerServiceRequiresHandler(RelationHandler): """ super().__init__(charm, relation_name, callback_f, mandatory) - def setup_event_handler(self) -> None: + def setup_event_handler(self) -> ops.Object: """Configure event handlers for Ceilometer service relation.""" import charms.ceilometer_k8s.v0.ceilometer_service as ceilometer_svc @@ -1483,9 +1533,11 @@ class CeilometerServiceRequiresHandler(RelationHandler): class CephAccessRequiresHandler(RelationHandler): """Handles the ceph access relation on the requires side.""" + interface: "ceph_access.CephAccessRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -1510,17 +1562,19 @@ class CephAccessRequiresHandler(RelationHandler): import charms.cinder_ceph_k8s.v0.ceph_access as ceph_access logger.debug("Setting up the ceph-access event handler") - ceph_access = sunbeam_tracing.trace_type( + ceph_access_requires = sunbeam_tracing.trace_type( ceph_access.CephAccessRequires )( self.charm, self.relation_name, ) - self.framework.observe(ceph_access.on.ready, self._ceph_access_ready) self.framework.observe( - ceph_access.on.goneaway, self._ceph_access_goneaway + ceph_access_requires.on.ready, self._ceph_access_ready ) - return ceph_access + self.framework.observe( + ceph_access_requires.on.goneaway, self._ceph_access_goneaway + ) + return ceph_access_requires def _ceph_access_ready(self, event: ops.framework.EventBase) -> None: """React to credential ready event.""" @@ -1556,10 +1610,12 @@ ExtraOpsProcess = Callable[[ops.EventBase, dict], None] class UserIdentityResourceRequiresHandler(RelationHandler): """Handle user management on IdentityResource relation.""" + interface: "identity_resource.IdentityResourceRequires" + CREDENTIALS_SECRET_PREFIX = "user-identity-resource-" CONFIGURE_SECRET_PREFIX = "configure-credential-" - resource_identifiers: FrozenSet[str] = frozenset( + resource_identifiers: frozenset[str] = frozenset( { "name", "email", @@ -1574,23 +1630,23 @@ class UserIdentityResourceRequiresHandler(RelationHandler): def __init__( self, - charm: ops.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool, name: str, domain: str, - email: Optional[str] = None, - description: Optional[str] = None, - project: Optional[str] = None, - project_domain: Optional[str] = None, + email: str | None = None, + description: str | None = None, + project: str | None = None, + project_domain: str | None = None, enable: bool = True, may_exist: bool = True, - role: Optional[str] = None, + role: str | None = None, add_suffix: bool = False, rotate: ops.SecretRotate = ops.SecretRotate.NEVER, - extra_ops: Optional[List[Union[dict, Callable]]] = None, - extra_ops_process: Optional[ExtraOpsProcess] = None, + extra_ops: list[dict | Callable] | None = None, + extra_ops_process: ExtraOpsProcess | None = None, ): self.username = name super().__init__(charm, relation_name, callback_f, mandatory) @@ -1698,20 +1754,23 @@ class UserIdentityResourceRequiresHandler(RelationHandler): label=self.label, rotate=self.rotate, ) + if not secret.id: + # We just created the secret, therefore id is always set + raise RuntimeError("Secret id not set") self.charm.leader_set({self.label: secret.id}) - return secret.id # type: ignore[union-attr] + return secret.id def _grant_ops_secret(self, relation: ops.Relation): secret = self.model.get_secret(id=self._ensure_credentials()) secret.grant(relation) - def _get_credentials(self) -> Tuple[str, str]: + def _get_credentials(self) -> tuple[str, str]: credentials_id = self._ensure_credentials() secret = self.model.get_secret(id=credentials_id) content = secret.get_content(refresh=True) return content["username"], content["password"] - def get_config_credentials(self) -> Optional[Tuple[str, str]]: + def get_config_credentials(self) -> tuple[str, str] | None: """Get credential from config secret.""" credentials_id = self.charm.leader_get(self.config_label) if not credentials_id: @@ -1732,6 +1791,9 @@ class UserIdentityResourceRequiresHandler(RelationHandler): secret = self.model.app.add_secret( content, label=self.config_label ) + if not secret.id: + # We just created the secret, therefore id is always set + raise RuntimeError("Secret id not set") self.charm.leader_set({self.config_label: secret.id}) return True @@ -1787,8 +1849,8 @@ class UserIdentityResourceRequiresHandler(RelationHandler): return request def _create_role_requests( - self, username, domain: Optional[str] - ) -> List[dict]: + self, username, domain: str | None + ) -> list[dict]: requests = [] if self.role: params = { @@ -1832,7 +1894,7 @@ class UserIdentityResourceRequiresHandler(RelationHandler): ) return requests - def _delete_user_request(self, users: List[str]) -> dict: + def _delete_user_request(self, users: list[str]) -> dict: requests = [] for user in users: params = {"name": user} @@ -1991,9 +2053,11 @@ class UserIdentityResourceRequiresHandler(RelationHandler): class CertificateTransferRequiresHandler(RelationHandler): """Handle certificate transfer relation on the requires side.""" + interface: "certificate_transfer.CertificateTransferRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -2016,7 +2080,7 @@ class CertificateTransferRequiresHandler(RelationHandler): """ super().__init__(charm, relation_name, callback_f, mandatory) - def setup_event_handler(self) -> None: + def setup_event_handler(self) -> ops.Object: """Configure event handlers for tls relation.""" logger.debug("Setting up certificate transfer event handler") @@ -2073,9 +2137,11 @@ class CertificateTransferRequiresHandler(RelationHandler): class TraefikRouteHandler(RelationHandler): """Base class to handle traefik route relations.""" + interface: "traefik_route.TraefikRouteRequirer" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -2094,7 +2160,7 @@ class TraefikRouteHandler(RelationHandler): interface = sunbeam_tracing.trace_type(TraefikRouteRequirer)( self.charm, - self.model.get_relation(self.relation_name), + self.model.get_relation(self.relation_name), # type: ignore # TraefikRouteRequirer has safeguards against None self.relation_name, ) @@ -2147,9 +2213,11 @@ class TraefikRouteHandler(RelationHandler): class NovaServiceRequiresHandler(RelationHandler): """Handle nova service relation on the requires side.""" + interface: "nova_service.NovaServiceRequires" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, callback_f: Callable, mandatory: bool = False, @@ -2172,7 +2240,7 @@ class NovaServiceRequiresHandler(RelationHandler): """ super().__init__(charm, relation_name, callback_f, mandatory) - def setup_event_handler(self) -> None: + def setup_event_handler(self) -> ops.Object: """Configure event handlers for Nova service relation.""" import charms.nova_k8s.v0.nova_service as nova_svc @@ -2216,9 +2284,11 @@ class NovaServiceRequiresHandler(RelationHandler): class LogForwardHandler(RelationHandler): """Handle log forward relation on the requires side.""" + interface: "loki_push_api.LogForwarder" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, mandatory: bool = False, ): @@ -2259,9 +2329,11 @@ class LogForwardHandler(RelationHandler): class TracingRequireHandler(RelationHandler): """Handle tracing relation on the requires side.""" + interface: "tracing.TracingEndpointRequirer" + def __init__( self, - charm: ops.charm.CharmBase, + charm: "OSBaseOperatorCharm", relation_name: str, mandatory: bool = False, protocols: list[str] | None = None, @@ -2297,10 +2369,11 @@ class TracingRequireHandler(RelationHandler): def tracing_endpoint(self) -> str | None: """Otlp endpoint for charm tracing.""" - if self.ready(): + if self.ready: return self.interface.get_endpoint("otlp_http") return None + @property def ready(self) -> bool: """Whether handler is ready for use.""" return self.interface.is_ready() diff --git a/ops-sunbeam/ops_sunbeam/templating.py b/ops-sunbeam/ops_sunbeam/templating.py index bc9a228b..048e2448 100644 --- a/ops-sunbeam/ops_sunbeam/templating.py +++ b/ops-sunbeam/ops_sunbeam/templating.py @@ -21,7 +21,6 @@ from pathlib import ( ) from typing import ( TYPE_CHECKING, - List, ) import ops.pebble @@ -35,15 +34,15 @@ import jinja2 log = logging.getLogger(__name__) -def get_container( - containers: List["ops.model.Container"], name: str -) -> "ops.model.Container": - """Search for container with given name inlist of containers.""" - container = None - for c in containers: - if c.name == name: - container = c - return container +# def get_container( +# containers: list[ops.model.Container], name: str +# ) -> ops.model.Container | None: +# """Search for container with given name inlist of containers.""" +# container = None +# for c in containers: +# if c.name == name: +# container = c +# return container def sidecar_config_render( diff --git a/ops-sunbeam/ops_sunbeam/test_utils.py b/ops-sunbeam/ops_sunbeam/test_utils.py index cb4331d7..ea7104fd 100644 --- a/ops-sunbeam/ops_sunbeam/test_utils.py +++ b/ops-sunbeam/ops_sunbeam/test_utils.py @@ -25,8 +25,12 @@ import sys import typing import unittest from typing import ( + BinaryIO, + Dict, List, Optional, + TextIO, + Union, ) from unittest.mock import ( MagicMock, @@ -35,6 +39,10 @@ from unittest.mock import ( ) import ops +import ops.storage +from ops_sunbeam.charm import ( + OSBaseOperatorCharm, +) sys.path.append("lib") # noqa sys.path.append("src") # noqa @@ -166,20 +174,22 @@ class ContainerCalls: def __init__(self) -> None: """Init container calls.""" - self.start = collections.defaultdict(list) - self.stop = collections.defaultdict(list) - self.push = collections.defaultdict(list) - self.pull = collections.defaultdict(list) - self.execute = collections.defaultdict(list) - self.remove_path = collections.defaultdict(list) + self.start: dict[str, list[list[str]]] = collections.defaultdict(list) + self.stop: dict[str, list[list[str]]] = collections.defaultdict(list) + self.push: dict[str, list[dict]] = collections.defaultdict(list) + self.pull: dict[str, list[str]] = collections.defaultdict(list) + self.execute: dict[str, list[list[str]]] = collections.defaultdict( + list + ) + self.remove_path: dict[str, list[str]] = collections.defaultdict(list) - def add_start(self, container_name: str, call: typing.Dict) -> None: + def add_start(self, container_name: str, call: list[str]) -> None: """Log a start call.""" self.start[container_name].append(call) - def add_stop(self, container_name: str, call: typing.Dict) -> None: - """Log a start call.""" - self.start[container_name].append(call) + def add_stop(self, container_name: str, call: list[str]) -> None: + """Log a stop call.""" + self.stop[container_name].append(call) def started_services(self, container_name: str) -> List: """Distinct unordered list of services that were started.""" @@ -205,15 +215,15 @@ class ContainerCalls: ) ) - def add_push(self, container_name: str, call: typing.Dict) -> None: + def add_push(self, container_name: str, call: dict) -> None: """Log a push call.""" self.push[container_name].append(call) - def add_pull(self, container_name: str, call: typing.Dict) -> None: + def add_pull(self, container_name: str, call: str) -> None: """Log a pull call.""" self.pull[container_name].append(call) - def add_execute(self, container_name: str, call: typing.List) -> None: + def add_execute(self, container_name: str, call: list[str]) -> None: """Log a execute call.""" self.execute[container_name].append(call) @@ -221,13 +231,11 @@ class ContainerCalls: """Log a remove path call.""" self.remove_path[container_name].append(call) - def updated_files(self, container_name: str) -> typing.List: + def updated_files(self, container_name: str) -> list: """Return a list of files that have been updated in a container.""" return [c["path"] for c in self.push.get(container_name, [])] - def file_update_calls( - self, container_name: str, file_name: str - ) -> typing.List: + def file_update_calls(self, container_name: str, file_name: str) -> list: """Return the update call for File_name in container_name.""" return [ c @@ -241,21 +249,21 @@ class CharmTestCase(unittest.TestCase): container_calls = ContainerCalls() - def setUp(self, obj: "typing.ANY", patches: "typing.List") -> None: + def setUp(self, obj: typing.Any, patches: list) -> None: # type: ignore """Run constructor.""" super().setUp() self.patches = patches self.obj = obj self.patch_all() - def patch(self, method: "typing.ANY") -> Mock: + def patch(self, method: typing.Any) -> Mock: """Patch the named method on self.obj.""" _m = patch.object(self.obj, method) mock = _m.start() self.addCleanup(_m.stop) return mock - def patch_obj(self, obj: "typing.ANY", method: "typing.ANY") -> Mock: + def patch_obj(self, obj: "typing.Any", method: "typing.Any") -> Mock: """Patch the named method on obj.""" _m = patch.object(obj, method) mock = _m.start() @@ -271,13 +279,13 @@ class CharmTestCase(unittest.TestCase): self, container: str, path: str, - contents: typing.List = None, - user: str = None, - group: str = None, - permissions: str = None, + contents: list | None = None, + user: str | None = None, + group: str | None = None, + permissions: str | None = None, ) -> None: """Check the attributes of a file.""" - client = self.harness.charm.unit.get_container(container)._pebble + client = self.harness.charm.unit.get_container(container)._pebble # type: ignore files = client.list_files(path, itself=True) self.assertEqual(len(files), 1) test_file = files[0] @@ -294,7 +302,7 @@ class CharmTestCase(unittest.TestCase): self.assertEqual(test_file.permissions, permissions) -def add_ingress_relation(harness: Harness, endpoint_type: str) -> str: +def add_ingress_relation(harness: Harness, endpoint_type: str) -> int: """Add ingress relation.""" app_name = "traefik-" + endpoint_type unit_name = app_name + "/0" @@ -305,7 +313,7 @@ def add_ingress_relation(harness: Harness, endpoint_type: str) -> str: def add_ingress_relation_data( - harness: Harness, rel_id: str, endpoint_type: str + harness: Harness, rel_id: int, endpoint_type: str ) -> None: """Add ingress data to ingress relation.""" app_name = "traefik-" + endpoint_type @@ -324,7 +332,7 @@ def add_complete_ingress_relation(harness: Harness) -> None: add_ingress_relation_data(harness, rel_id, endpoint_type) -def add_base_amqp_relation(harness: Harness) -> str: +def add_base_amqp_relation(harness: Harness) -> int: """Add amqp relation.""" rel_id = harness.add_relation("amqp", "rabbitmq") harness.add_relation_unit(rel_id, "rabbitmq/0") @@ -335,7 +343,7 @@ def add_base_amqp_relation(harness: Harness) -> str: return rel_id -def add_amqp_relation_credentials(harness: Harness, rel_id: str) -> None: +def add_amqp_relation_credentials(harness: Harness, rel_id: int) -> None: """Add amqp data to amqp relation.""" harness.update_relation_data( rel_id, @@ -344,7 +352,7 @@ def add_amqp_relation_credentials(harness: Harness, rel_id: str) -> None: ) -def add_base_ceph_access_relation(harness: Harness) -> str: +def add_base_ceph_access_relation(harness: Harness) -> int: """Add ceph-access relation.""" rel_id = harness.add_relation( "ceph-access", "cinder-ceph", app_data={"a": "b"} @@ -352,7 +360,7 @@ def add_base_ceph_access_relation(harness: Harness) -> str: return rel_id -def add_ceph_access_relation_response(harness: Harness, rel_id: str) -> None: +def add_ceph_access_relation_response(harness: Harness, rel_id: int) -> None: """Add secret data to cinder-access relation.""" credentials_content = {"uuid": "svcuser1", "key": "svcpass1"} credentials_id = harness.add_model_secret( @@ -364,7 +372,7 @@ def add_ceph_access_relation_response(harness: Harness, rel_id: str) -> None: ) -def add_base_identity_service_relation(harness: Harness) -> str: +def add_base_identity_service_relation(harness: Harness) -> int: """Add identity-service relation.""" rel_id = harness.add_relation("identity-service", "keystone") harness.add_relation_unit(rel_id, "keystone/0") @@ -376,7 +384,7 @@ def add_base_identity_service_relation(harness: Harness) -> str: def add_identity_service_relation_response( - harness: Harness, rel_id: str + harness: Harness, rel_id: int ) -> None: """Add id service data to identity-service relation.""" credentials_content = {"username": "svcuser1", "password": "svcpass1"} @@ -408,7 +416,7 @@ def add_identity_service_relation_response( ) -def add_base_identity_credentials_relation(harness: Harness) -> str: +def add_base_identity_credentials_relation(harness: Harness) -> int: """Add identity-service relation.""" rel_id = harness.add_relation("identity-credentials", "keystone") harness.add_relation_unit(rel_id, "keystone/0") @@ -420,7 +428,7 @@ def add_base_identity_credentials_relation(harness: Harness) -> str: def add_identity_credentials_relation_response( - harness: Harness, rel_id: str + harness: Harness, rel_id: int ) -> None: """Add id service data to identity-service relation.""" credentials_content = {"username": "username", "password": "user-password"} @@ -451,7 +459,7 @@ def add_identity_credentials_relation_response( ) -def add_base_db_relation(harness: Harness) -> str: +def add_base_db_relation(harness: Harness) -> int: """Add db relation.""" rel_id = harness.add_relation("database", "mysql") harness.add_relation_unit(rel_id, "mysql/0") @@ -462,7 +470,7 @@ def add_base_db_relation(harness: Harness) -> str: return rel_id -def add_db_relation_credentials(harness: Harness, rel_id: str) -> None: +def add_db_relation_credentials(harness: Harness, rel_id: int) -> None: """Add db credentials data to db relation.""" secret_id = harness.add_model_secret( "mysql", {"username": "foo", "password": "hardpassword"} @@ -487,35 +495,35 @@ def add_api_relations(harness: Harness) -> None: ) -def add_complete_db_relation(harness: Harness) -> None: +def add_complete_db_relation(harness: Harness) -> int: """Add complete DB relation.""" rel_id = add_base_db_relation(harness) add_db_relation_credentials(harness, rel_id) return rel_id -def add_complete_identity_relation(harness: Harness) -> None: +def add_complete_identity_relation(harness: Harness) -> int: """Add complete Identity relation.""" rel_id = add_base_identity_service_relation(harness) add_identity_service_relation_response(harness, rel_id) return rel_id -def add_complete_identity_credentials_relation(harness: Harness) -> None: +def add_complete_identity_credentials_relation(harness: Harness) -> int: """Add complete identity-credentials relation.""" rel_id = add_base_identity_credentials_relation(harness) add_identity_credentials_relation_response(harness, rel_id) return rel_id -def add_complete_amqp_relation(harness: Harness) -> None: +def add_complete_amqp_relation(harness: Harness) -> int: """Add complete AMQP relation.""" rel_id = add_base_amqp_relation(harness) add_amqp_relation_credentials(harness, rel_id) return rel_id -def add_ceph_relation_credentials(harness: Harness, rel_id: str) -> None: +def add_ceph_relation_credentials(harness: Harness, rel_id: int) -> None: """Add amqp data to amqp relation.""" # During tests the charm class is never destroyed and recreated as it # would be between hook executions. This means request is never marked @@ -546,7 +554,7 @@ def add_ceph_relation_credentials(harness: Harness, rel_id: str) -> None: harness.add_relation_unit(rel_id, "ceph-mon/1") -def add_base_ceph_relation(harness: Harness) -> str: +def add_base_ceph_relation(harness: Harness) -> int: """Add identity-service relation.""" rel_id = harness.add_relation("ceph", "ceph-mon") harness.add_relation_unit(rel_id, "ceph-mon/0") @@ -556,14 +564,14 @@ def add_base_ceph_relation(harness: Harness) -> str: return rel_id -def add_complete_ceph_relation(harness: Harness) -> None: +def add_complete_ceph_relation(harness: Harness) -> int: """Add complete ceph relation.""" rel_id = add_base_ceph_relation(harness) add_ceph_relation_credentials(harness, rel_id) return rel_id -def add_certificates_relation_certs(harness: Harness, rel_id: str) -> None: +def add_certificates_relation_certs(harness: Harness, rel_id: int) -> None: """Add cert data to certificates relation.""" cert = { "certificate": TEST_SERVER_CERT, @@ -576,7 +584,7 @@ def add_certificates_relation_certs(harness: Harness, rel_id: str) -> None: ) -def add_base_certificates_relation(harness: Harness) -> str: +def add_base_certificates_relation(harness: Harness) -> int: """Add certificates relation.""" rel_id = harness.add_relation("certificates", "vault") harness.add_relation_unit(rel_id, "vault/0") @@ -593,14 +601,14 @@ def add_base_certificates_relation(harness: Harness) -> str: return rel_id -def add_complete_certificates_relation(harness: Harness) -> None: +def add_complete_certificates_relation(harness: Harness) -> int: """Add complete certificates relation.""" rel_id = add_base_certificates_relation(harness) add_certificates_relation_certs(harness, rel_id) return rel_id -def add_complete_peer_relation(harness: Harness) -> None: +def add_complete_peer_relation(harness: Harness) -> int: """Add complete peer relation.""" rel_id = harness.add_relation("peers", harness.charm.app.name) new_unit = f"{harness.charm.app.name}/1" @@ -643,12 +651,12 @@ test_relations = { } -def add_all_relations(harness: Harness) -> None: +def add_all_relations(harness: Harness) -> dict[str, int]: """Add all the relations there are test relations for.""" rel_ids = {} for key in harness._meta.relations.keys(): - if test_relations.get(key): - rel_id = test_relations[key](harness) + if add_complete_relation := test_relations.get(key): + rel_id = add_complete_relation(harness) rel_ids[key] = rel_id return rel_ids @@ -670,50 +678,52 @@ def set_remote_leader_ready( def get_harness( - charm_class: ops.charm.CharmBase, - charm_metadata: str = None, - container_calls: dict = None, - charm_config: str = None, - charm_actions: str = None, - initial_charm_config: dict = None, + charm_class: type[OSBaseOperatorCharm], + charm_metadata: str | None = None, + container_calls: ContainerCalls | None = None, + charm_config: str | None = None, + charm_actions: str | None = None, + initial_charm_config: dict | None = None, ) -> Harness: """Return a testing harness.""" + if container_calls is None: + container_calls = ContainerCalls() class _OSTestingPebbleClient(_TestingPebbleClient): + container_name: str + def exec( self, - command: typing.List[str], + command: list[str], *, service_context: Optional[str] = None, - environment: typing.Dict[str, str] = None, - working_dir: str = None, - timeout: float = None, - user_id: int = None, - user: str = None, - group_id: int = None, - group: str = None, - stdin: typing.Union[ - str, bytes, typing.TextIO, typing.BinaryIO - ] = None, - stdout: typing.Union[typing.TextIO, typing.BinaryIO] = None, - stderr: typing.Union[typing.TextIO, typing.BinaryIO] = None, - encoding: str = "utf-8", + environment: Optional[Dict[str, str]] = None, + working_dir: Optional[str] = None, + timeout: Optional[float] = None, + user_id: Optional[int] = None, + user: Optional[str] = None, + group_id: Optional[int] = None, + group: Optional[str] = None, + stdin: Optional[Union[str, bytes, TextIO, BinaryIO]] = None, + stdout: Optional[Union[TextIO, BinaryIO]] = None, + stderr: Optional[Union[TextIO, BinaryIO]] = None, + encoding: Optional[str] = "utf-8", combine_stderr: bool = False, - ) -> None: - container_calls.add_execute(self.container_name, command) + ) -> ops.pebble.ExecProcess[typing.Any]: + container_calls.add_execute(self.container_name, command) # type: ignore process_mock = MagicMock() process_mock.wait_output.return_value = ("", None) return process_mock def start_services( self, - services: List[str], + services: list[str], timeout: float = 30.0, delay: float = 0.1, ) -> None: """Record start service events.""" super().start_services(services, timeout, delay) - container_calls.add_start(self.container_name, services) + container_calls.add_start(self.container_name, services) # type: ignore def stop_services( self, @@ -723,7 +733,7 @@ def get_harness( ) -> None: """Record stop service events.""" super().stop_services(services, timeout, delay) - container_calls.add_stop(self.container_name, services) + container_calls.add_stop(self.container_name, services) # type: ignore class _OSTestingModelBackend(_TestingModelBackend): def get_pebble(self, socket_path: str) -> _OSTestingPebbleClient: @@ -749,10 +759,10 @@ def get_harness( self._pebble_clients[container] = client self._pebble_clients_can_connect[client] = False - return client + return client # type: ignore - def network_get( - self, endpoint_name: str, relation_id: str = None + def network_get( # type: ignore + self, endpoint_name: str, relation_id: int | None = None ) -> dict: """Return a fake set of network data.""" network_data = { @@ -769,7 +779,7 @@ def get_harness( } return network_data - filename = inspect.getfile(charm_class) + filename = inspect.getfile(charm_class) # type: ignore # Use pathlib.Path(filename).parents[1] if tests structure is # /unit_tests # Use pathlib.Path(filename).parents[2] if tests structure is @@ -792,9 +802,12 @@ def get_harness( harness._backend = _OSTestingModelBackend( harness._unit_name, harness._meta, harness._get_config(charm_config) ) - harness._model = model.Model(harness._meta, harness._backend) + harness._model = model.Model(harness._meta, harness._backend) # type: ignore harness._framework = framework.Framework( - ":memory:", harness._charm_dir, harness._meta, harness._model + ops.storage.SQLiteStorage(":memory:"), + harness._charm_dir, + harness._meta, + harness._model, ) harness.set_model_name("test-model") diff --git a/ops-sunbeam/ops_sunbeam/tracing.py b/ops-sunbeam/ops_sunbeam/tracing.py index 3cf653e1..8424ab8d 100644 --- a/ops-sunbeam/ops_sunbeam/tracing.py +++ b/ops-sunbeam/ops_sunbeam/tracing.py @@ -24,7 +24,7 @@ from typing import ( overload, ) -_T = TypeVar("_T") +_T = TypeVar("_T", bound=type) try: from charms.tempo_k8s.v1.charm_tracing import ( diff --git a/run_tox.sh b/run_tox.sh index 5ab28199..5d5a28fe 100755 --- a/run_tox.sh +++ b/run_tox.sh @@ -62,6 +62,11 @@ then pflake8 --config pyproject.toml ${src_path} ${tst_path} ${ops_sunbeam_src_path} ${ops_sunbeam_tst_path} || exit 1 isort --check-only --diff ${src_path} ${tst_path} ${ops_sunbeam_src_path} ${ops_sunbeam_tst_path} || exit 1 black --config pyproject.toml --check --diff ${src_path} ${tst_path} ${ops_sunbeam_src_path} ${ops_sunbeam_tst_path} || exit 1 +elif [[ $1 == "linters" ]] +then + ops_sunbeam_src_path="ops-sunbeam/ops_sunbeam" + + PYTHONPATH=$(python3 ./repository.py pythonpath) mypy ${ops_sunbeam_src_path} elif [[ $1 =~ ^(py3|py310|py311)$ ]]; then diff --git a/tox.ini b/tox.ini index b3a39c1f..efd86fab 100644 --- a/tox.ini +++ b/tox.ini @@ -70,6 +70,13 @@ deps = {[testenv:py3]deps} commands = {toxinidir}/run_tox.sh cover {posargs} +[testenv:linters] +deps = + {[testenv:py3]deps} + mypy +commands = + {toxinidir}/run_tox.sh linters + [testenv:build] basepython = python3 deps = pyyaml diff --git a/zuul.d/project-templates.yaml b/zuul.d/project-templates.yaml index 8beeb170..80276bb1 100644 --- a/zuul.d/project-templates.yaml +++ b/zuul.d/project-templates.yaml @@ -8,6 +8,7 @@ version 3 releases designated for testing the latest release. check: jobs: + - openstack-tox-linters - openstack-tox-pep8 - openstack-tox-py310: branches: @@ -19,6 +20,7 @@ - main gate: jobs: + - openstack-tox-linters - openstack-tox-pep8 - openstack-tox-py310: branches: