From a48f7ce307e01c69f9e34b3c14ecf2404c2b819d Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Tue, 12 Nov 2024 16:20:54 -0600 Subject: [PATCH 1/3] Support kube-control v2 schema --- .github/workflows/ci.yaml | 4 ++-- requirements.txt | 4 ++-- src/charm.py | 10 +++++++--- tests/unit/test_charm.py | 13 +++++++++++-- tox.ini | 2 +- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 346fa4a..74d158d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ on: jobs: call-inclusive-naming-check: name: Inclusive Naming - uses: canonical-web-and-design/Inclusive-naming/.github/workflows/woke.yaml@main + uses: canonical/inclusive-naming/.github/workflows/woke.yaml@main with: fail-on-error: "true" @@ -16,7 +16,7 @@ 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']" needs: - call-inclusive-naming-check diff --git a/requirements.txt b/requirements.txt index d9a7819..a253cfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ backports.cached-property -ops>=1.3.0,<2.0.0 +ops lightkube>=0.10.1,<1.0.0 pyyaml pydantic==1.* ops.manifest>=1.1.0,<2.0.0 -git+https://github.com/charmed-kubernetes/interface-kube-control.git@6dd289d1c795fdeda1bed17873b8d6562227c829#subdirectory=ops +git+https://github.com/charmed-kubernetes/interface-kube-control.git@KU-1998/use-secrets#subdirectory=ops git+https://github.com/charmed-kubernetes/interface-tls-certificates.git@339efe3823b9728d16cdf5bcd1fc3b5de4e68923#subdirectory=ops \ No newline at end of file diff --git a/src/charm.py b/src/charm.py index 076d3cb..3031ff5 100755 --- a/src/charm.py +++ b/src/charm.py @@ -31,7 +31,7 @@ def __init__(self, *args): super().__init__(*args) # Relation Validator and datastore - self.kube_control = KubeControlRequirer(self) + self.kube_control = KubeControlRequirer(self, schemas="0,1") self.certificates = CertificatesRequires(self) # Config Validator and datastore self.charm_config = CharmConfig(self) @@ -113,7 +113,7 @@ def _update_status(self, _): self.app.status = 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_kube_control(self, event): @@ -137,6 +137,10 @@ def _check_kube_control(self, event): return True def _check_certificates(self, event): + if self.kube_control.get_ca_certificate(): + log.info("CA Certificate is available from kube-control.") + return True + self.unit.status = MaintenanceStatus("Evaluating certificates.") evaluation = self.certificates.evaluate_relation(event) if evaluation: @@ -192,7 +196,7 @@ def _install_or_upgrade(self, event, config_hash=None): controller.apply_manifests() except ManifestClientError as e: self.unit.status = WaitingStatus("Waiting for kube-apiserver") - log.warn(f"Encountered retryable installation error: {e}") + log.warning("Encountered retryable installation error: %s", e) event.defer() return False return True diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index d0fb066..6250aa0 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -53,11 +53,20 @@ def kube_control(): kube_control.get_controller_labels.return_value = [] kube_control.get_cluster_tag.return_value = "kubernetes-4ypskxahbu3rnfgsds3pksvwe3uh0lxt" kube_control.get_cluster_cidr.return_value = ip_network("192.168.0.0/16") + kube_control.get_ca_certificate.return_value = None kube_control.relation.app.name = "kubernetes-control-plane" kube_control.relation.units = [f"kubernetes-control-plane/{_}" for _ in range(2)] yield kube_control +def test_waits_for_relations(harness): + harness.begin_with_initial_hooks() + charm = harness.charm + assert isinstance(charm.unit.status, BlockedStatus) + assert charm.unit.status.message == "Missing required certificates" + + +@pytest.mark.usefixtures("kube_control") def test_waits_for_certificates(harness): harness.begin_with_initial_hooks() charm = harness.charm @@ -80,8 +89,8 @@ def test_waits_for_certificates(harness): "easyrsa/0", yaml.safe_load(Path("tests/data/certificates_data.yaml").read_text()), ) - assert isinstance(charm.unit.status, BlockedStatus) - assert charm.unit.status.message == "Missing required kube-control relation" + assert isinstance(charm.unit.status, MaintenanceStatus) + assert charm.unit.status.message == "Deploying AWS Cloud Provider" @mock.patch("ops.interface_kube_control.KubeControlRequirer.create_kubeconfig") diff --git a/tox.ini b/tox.ini index 80f661e..b521b37 100644 --- a/tox.ini +++ b/tox.ini @@ -37,7 +37,7 @@ commands = description = Check code against coding style standards deps = black - flake8 < 5.0 # TODO: https://github.com/csachs/pyproject-flake8/issues/13 + flake8 flake8-docstrings flake8-copyright flake8-builtins From cfb521acb1bec61cdafe0e360731afa0b9076222 Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Tue, 12 Nov 2024 22:25:03 -0600 Subject: [PATCH 2/3] Fetch kubeconfig via action for integration tests --- tests/integration/conftest.py | 46 ++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8bad1a6..d9c33eb 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,5 +1,6 @@ # Copyright 2022 Canonical Ltd. # See LICENSE file for licensing details. +import asyncio import logging import random import string @@ -9,6 +10,7 @@ from lightkube import AsyncClient, KubeConfig from lightkube.models.meta_v1 import ObjectMeta from lightkube.resources.core_v1 import Namespace +from pytest_operator.plugin import OpsTest log = logging.getLogger(__name__) @@ -18,20 +20,40 @@ def module_name(request): return request.module.__name__.replace("_", "-") +async def get_leader(app): + """Find leader unit of an application. + + Args: + app: Juju application + + Returns: + int: index to leader unit + """ + is_leader = await asyncio.gather(*(u.is_leader_from_status() for u in app.units)) + for idx, flag in enumerate(is_leader): + if flag: + return idx + + @pytest.fixture() -async def kubeconfig(ops_test): +async def kubeconfig(ops_test: OpsTest): + for choice in ["kubernetes-control-plane", "k8s"]: + if app := ops_test.model.applications.get(choice): + break + else: + pytest.fail("No kubernetes-control-plane or k8s application found") + leader_idx = await get_leader(app) + leader = app.units[leader_idx] + kubeconfig_path = ops_test.tmp_path / "kubeconfig" - retcode, stdout, stderr = await ops_test.run( - "juju", - "scp", - "kubernetes-control-plane/leader:/home/ubuntu/config", - kubeconfig_path, - ) + action = await leader.run_action("get-kubeconfig") + data = await action.wait() + retcode, kubeconfig = (data.results.get(key, {}) for key in ["return-code", "kubeconfig"]) + if retcode != 0: - log.error(f"retcode: {retcode}") - log.error(f"stdout:\n{stdout.strip()}") - log.error(f"stderr:\n{stderr.strip()}") - pytest.fail("Failed to copy kubeconfig from kubernetes-control-plane") + log.error("Failed to copy kubeconfig from %s (%s)", app.name, data.results) + pytest.fail(f"Failed to copy kubeconfig from {app.name}") + kubeconfig_path.write_text(kubeconfig) assert Path(kubeconfig_path).stat().st_size, "kubeconfig file is 0 bytes" yield kubeconfig_path @@ -42,7 +64,7 @@ async def kubernetes(kubeconfig, module_name): namespace = f"{module_name}-{rand_str}" config = KubeConfig.from_file(kubeconfig) client = AsyncClient( - config=config.get(context_name="juju-context"), + config=config.get(context_name=config.current_context), namespace=namespace, trust_env=False, ) From 0301d813c56351926097632b6d10a6d2ed638e6e Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Fri, 15 Nov 2024 22:26:04 -0600 Subject: [PATCH 3/3] Update requirements.txt --- requirements.txt | 4 ++-- src/charm.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index a253cfa..60d4450 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ lightkube>=0.10.1,<1.0.0 pyyaml pydantic==1.* ops.manifest>=1.1.0,<2.0.0 -git+https://github.com/charmed-kubernetes/interface-kube-control.git@KU-1998/use-secrets#subdirectory=ops -git+https://github.com/charmed-kubernetes/interface-tls-certificates.git@339efe3823b9728d16cdf5bcd1fc3b5de4e68923#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 diff --git a/src/charm.py b/src/charm.py index 3031ff5..8112bc7 100755 --- a/src/charm.py +++ b/src/charm.py @@ -6,11 +6,11 @@ import logging from pathlib import Path +import ops from ops.charm import CharmBase from ops.framework import StoredState from ops.interface_kube_control import KubeControlRequirer from ops.interface_tls_certificates import CertificatesRequires -from ops.main import main from ops.manifests import Collector, ManifestClientError from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus @@ -215,4 +215,4 @@ def _cleanup(self, event): if __name__ == "__main__": - main(AwsCloudProviderCharm) + ops.main(AwsCloudProviderCharm)