From 0fb3e72218a1d61cc084aab6f6bbe66d2e33a379 Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Fri, 6 Dec 2024 10:56:14 -0600 Subject: [PATCH] Support kube-control v2 schema --- .github/workflows/ci.yaml | 2 +- requirements.txt | 7 +- src/charm.py | 67 +++++++++--------- src/requires_certificates.py | 131 ----------------------------------- 4 files changed, 39 insertions(+), 168 deletions(-) delete mode 100644 src/requires_certificates.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fab4f83..33ff0c3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,4 +12,4 @@ jobs: name: Lint Unit uses: charmed-kubernetes/workflows/.github/workflows/lint-unit.yaml@main with: - python: "['3.8', '3.9', '3.10', '3.11']" + python: "['3.8', '3.10', '3.12']" diff --git a/requirements.txt b/requirements.txt index 9885f93..11e3684 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ lightkube>=0.10.1,<1.0.0 -ops>=1.3.0,<2.0.0 +ops ops.manifest>=1.1.0,<2.0.0 pydantic==1.* -git+https://github.com/charmed-kubernetes/interface-kube-control.git@6dd289d1c795fdeda1bed17873b8d6562227c829#subdirectory=ops -git+https://github.com/charmed-kubernetes/interface-openstack-integration.git@066d177e8ef1acb1e7b1d92832a32c9a1fb41c9e#subdirectory=ops \ No newline at end of file +ops.interface-kube-control @ git+https://github.com/charmed-kubernetes/interface-kube-control.git@edc07bce7ea4c25d472fa4d95834602a7ebce5cd#subdirectory=ops +ops.interface-tls-certificates @ git+https://github.com/charmed-kubernetes/interface-tls-certificates.git@4a1081da098154b96337a09c8e9c40acff2d330e#subdirectory=ops +ops.interface-openstack-integration @ git+https://github.com/charmed-kubernetes/interface-openstack-integration.git@066d177e8ef1acb1e7b1d92832a32c9a1fb41c9e#subdirectory=ops diff --git a/src/charm.py b/src/charm.py index 09b77f1..47062d1 100755 --- a/src/charm.py +++ b/src/charm.py @@ -7,25 +7,22 @@ import os from pathlib import Path -from ops.charm import CharmBase -from ops.framework import StoredState +import ops from ops.interface_kube_control import KubeControlRequirer from ops.interface_openstack_integration import OpenstackIntegrationRequirer -from ops.main import main +from ops.interface_tls_certificates import CertificatesRequires from ops.manifests import Collector, ManifestClientError -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus from config import CharmConfig -from requires_certificates import CertificatesRequires from storage_manifests import StorageManifests log = logging.getLogger(__name__) -class CinderCSICharm(CharmBase): +class CinderCSICharm(ops.CharmBase): """Deploy and manage the Cinder CSI plugin for K8s on OpenStack.""" - stored = StoredState() + stored = ops.StoredState() def __init__(self, *args): super().__init__(*args) @@ -34,7 +31,7 @@ def __init__(self, *args): self._kubeconfig_path.parent.mkdir(parents=True, exist_ok=True) # Relation Validator and datastore - self.kube_control = KubeControlRequirer(self) + self.kube_control = KubeControlRequirer(self, schemas="0,1") self.certificates = CertificatesRequires(self) self.integrator = OpenstackIntegrationRequirer(self) # Config Validator and datastore @@ -111,38 +108,38 @@ def _update_status(self, _): unready = self.collector.unready if unready: - self.unit.status = WaitingStatus(", ".join(unready)) + self.unit.status = ops.WaitingStatus(", ".join(unready)) else: - self.unit.status = ActiveStatus("Ready") + self.unit.status = ops.ActiveStatus("Ready") self.unit.set_workload_version(self.collector.short_version) - self.app.status = ActiveStatus(self.collector.long_version) + self.app.status = ops.ActiveStatus(self.collector.long_version) def _kube_control(self, event): - self.kube_control.set_auth_request(self.unit.name) + self.kube_control.set_auth_request(self.unit.name, "system:masters") return self._merge_config(event) def _check_integrator(self, event): - self.unit.status = MaintenanceStatus("Evaluating Openstack relation.") + self.unit.status = ops.MaintenanceStatus("Evaluating Openstack relation.") evaluation = self.integrator.evaluate_relation(event) if evaluation: if "Waiting" in evaluation: - self.unit.status = WaitingStatus(evaluation) + self.unit.status = ops.WaitingStatus(evaluation) else: - self.unit.status = BlockedStatus(evaluation) + self.unit.status = ops.BlockedStatus(evaluation) return False return True def _check_kube_control(self, event): - self.unit.status = MaintenanceStatus("Evaluating kubernetes authentication.") + self.unit.status = ops.MaintenanceStatus("Evaluating kubernetes authentication.") evaluation = self.kube_control.evaluate_relation(event) if evaluation: if "Waiting" in evaluation: - self.unit.status = WaitingStatus(evaluation) + self.unit.status = ops.WaitingStatus(evaluation) else: - self.unit.status = BlockedStatus(evaluation) + self.unit.status = ops.BlockedStatus(evaluation) return False if not self.kube_control.get_auth_credentials(self.unit.name): - self.unit.status = WaitingStatus("Waiting for kube-control: unit credentials") + self.unit.status = ops.WaitingStatus("Waiting for kube-control: unit credentials") return False self.kube_control.create_kubeconfig( self._ca_cert_path, self._kubeconfig_path, "root", self.unit.name @@ -150,22 +147,26 @@ def _check_kube_control(self, event): return True def _check_certificates(self, event): - self.unit.status = MaintenanceStatus("Evaluating certificates.") + if self.kube_control.get_ca_certificate(): + log.info("CA Certificate is available from kube-control.") + return True + + self.unit.status = ops.MaintenanceStatus("Evaluating certificates.") evaluation = self.certificates.evaluate_relation(event) if evaluation: if "Waiting" in evaluation: - self.unit.status = WaitingStatus(evaluation) + self.unit.status = ops.WaitingStatus(evaluation) else: - self.unit.status = BlockedStatus(evaluation) + self.unit.status = ops.BlockedStatus(evaluation) return False self._ca_cert_path.write_text(self.certificates.ca) return True def _check_config(self): - self.unit.status = MaintenanceStatus("Evaluating charm config.") + self.unit.status = ops.MaintenanceStatus("Evaluating charm config.") evaluation = self.charm_config.evaluate() if evaluation: - self.unit.status = BlockedStatus(evaluation) + self.unit.status = ops.BlockedStatus(evaluation) return False return True @@ -182,12 +183,12 @@ def _merge_config(self, event): if not self._check_config(): return - self.unit.status = MaintenanceStatus("Evaluating Manifests") + self.unit.status = ops.MaintenanceStatus("Evaluating Manifests") new_hash = 0 for controller in self.collector.manifests.values(): evaluation = controller.evaluate() if evaluation: - self.unit.status = BlockedStatus(evaluation) + self.unit.status = ops.BlockedStatus(evaluation) return new_hash += controller.hash() @@ -201,31 +202,31 @@ def _install_or_upgrade(self, event, config_hash=None): log.info("Skipping until the config is evaluated.") return True - self.unit.status = MaintenanceStatus("Deploying Cinder Storage") + self.unit.status = ops.MaintenanceStatus("Deploying Cinder Storage") self.unit.set_workload_version("") for controller in self.collector.manifests.values(): try: controller.apply_manifests() except ManifestClientError as e: - self.unit.status = WaitingStatus("Waiting for kube-apiserver") - log.warn(f"Encountered retryable installation error: {e}") + self.unit.status = ops.WaitingStatus("Waiting for kube-apiserver") + log.warning("Encountered retryable installation error: %s", e) event.defer() return False return True def _cleanup(self, event): if self.stored.config_hash: - self.unit.status = MaintenanceStatus("Cleaning up Openstack Storage") + self.unit.status = ops.MaintenanceStatus("Cleaning up Openstack Storage") for controller in self.collector.manifests.values(): try: controller.delete_manifests(ignore_unauthorized=True) except ManifestClientError: - self.unit.status = WaitingStatus("Waiting for kube-apiserver") + self.unit.status = ops.WaitingStatus("Waiting for kube-apiserver") event.defer() return - self.unit.status = MaintenanceStatus("Shutting down") + self.unit.status = ops.MaintenanceStatus("Shutting down") self._kubeconfig_path.parent.unlink(missing_ok=True) if __name__ == "__main__": - main(CinderCSICharm) + ops.main(CinderCSICharm) diff --git a/src/requires_certificates.py b/src/requires_certificates.py deleted file mode 100644 index 6c84edc..0000000 --- a/src/requires_certificates.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2022 Canonical Ltd. -# See LICENSE file for licensing details. -"""Implementation of tls-certificates interface. - -This only implements the requires side, currently, since the providers -is still using the Reactive Charm framework self. -""" -import json -import logging -from typing import List, Mapping, Optional - -from backports.cached_property import cached_property -from ops.charm import RelationBrokenEvent -from ops.framework import Object -from pydantic import BaseModel, Field, ValidationError - -log = logging.getLogger(__name__) - - -class Certificate(BaseModel): - """Represent a Certificate.""" - - cert_type: str - common_name: str - cert: str - key: str - - -class Data(BaseModel): - """Databag from the relation.""" - - ca: str = Field(alias="ca") - client_cert: str = Field(alias="client.cert") - client_key: str = Field(alias="client.key") - - -class CertificatesRequires(Object): - """Requires side of certificates relation.""" - - def __init__(self, charm, endpoint="certificates"): - super().__init__(charm, f"relation-{endpoint}") - self.endpoint = endpoint - events = charm.on[endpoint] - self._unit_name = self.model.unit.name.replace("/", "_") - self.framework.observe(events.relation_joined, self._joined) - - def _joined(self, event=None): - event.relation.data[self.model.unit]["unit-name"] = self._unit_name - - @cached_property - def relation(self): - """The relation to the integrator, or None.""" - return self.model.get_relation(self.endpoint) - - @cached_property - def _raw_data(self): - if self.relation and self.relation.units: - return self.relation.data[list(self.relation.units)[0]] - return None - - @cached_property - def _data(self) -> Optional[Data]: - raw = self._raw_data - return Data(**raw) if raw else None - - def evaluate_relation(self, event) -> Optional[str]: - """Determine if relation is ready.""" - no_relation = not self.relation or ( - isinstance(event, RelationBrokenEvent) and event.relation is self.relation - ) - if not self.is_ready: - if no_relation: - return f"Missing required {self.endpoint}" - return f"Waiting for {self.endpoint}" - return None - - @property - def is_ready(self): - """Whether the request for this instance has been completed.""" - try: - self._data - except ValidationError as ve: - log.error(f"{self.endpoint} relation data not yet valid. ({ve}") - return False - if self._data is None: - log.error(f"{self.endpoint} relation data not yet available.") - return False - return True - - @property - def ca(self): - """The ca value.""" - if not self.is_ready: - return None - - return self._data.ca - - @property - def client_certs(self) -> List[Certificate]: - """Certificate instances for all available client certs.""" - if not self.is_ready: - return [] - - field = "{}.processed_client_requests".format(self._unit_name) - certs_data = self._raw_data.get(field, {}) - return [ - Certificate(cert_type="client", common_name=common_name, **cert) - for common_name, cert in certs_data.items() - ] - - @property - def client_certs_map(self) -> Mapping[str, Certificate]: - """Certificate instances by their `common_name`.""" - return {cert.common_name: cert for cert in self.client_certs} - - def request_client_cert(self, cn, sans): - """Request Client certificate for charm. - - Request a client certificate and key be generated for the given - common name (`cn`) and list of alternative names (`sans`). - This can be called multiple times to request more than one client - certificate, although the common names must be unique. If called - again with the same common name, it will be ignored. - """ - if not self.relation: - return - # assume we'll only be connected to one provider - data = self.relation.data[self.charm.unit] - requests = data.get("client_cert_requests", {}) - requests[cn] = {"sans": sans} - data["client_cert_requests"] = json.dumps(requests)