From 75c50a1e183c5a9ffc20fb1b6a35069d0ddc3085 Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Tue, 19 Aug 2025 17:27:42 +0530 Subject: [PATCH] Add support for magnum-capi-helm * Add kubeconfig as configuration option to charm * Update magnum.conf templates to add new configuration related to magnum-capi-helm driver Change-Id: Id2eacca3cb189be5507f29f84ebcce73c0c201a5 Signed-off-by: Hemanth Nakkina --- charms/magnum-k8s/README.md | 21 +++++++ charms/magnum-k8s/charmcraft.yaml | 5 ++ charms/magnum-k8s/src/charm.py | 46 +++++++++++++- charms/magnum-k8s/src/templates/kubeconfig.j2 | 1 + .../magnum-k8s/src/templates/magnum.conf.j2 | 12 ++++ .../tests/unit/test_magnum_charm.py | 14 ++++- tests/all-k8s/tests.yaml | 5 +- .../zaza/sunbeam/charm_tests/magnum/setup.py | 61 +++++++++++++++++++ tox.ini | 1 + 9 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 charms/magnum-k8s/src/templates/kubeconfig.j2 create mode 100644 tests/local/zaza/sunbeam/charm_tests/magnum/setup.py diff --git a/charms/magnum-k8s/README.md b/charms/magnum-k8s/README.md index 23bd9ce0..2b3dde5e 100644 --- a/charms/magnum-k8s/README.md +++ b/charms/magnum-k8s/README.md @@ -34,6 +34,27 @@ Actions allow specific operations to be performed on a per-unit basis. To display action descriptions run `juju actions magnum`. If the charm is not deployed then see file `actions.yaml`. +### Further information on testing + +magnum-k8s support magnum-capi-helm driver and needs external kubernetes management +cluster. + +Kubernetes management cluster should already have the Cluster API deployed. +Cluster API can be deployed by running following steps + + curl -L https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.9.6/clusterctl-linux-amd64 -o clusterctl + sudo install -o root -g root -m 0755 clusterctl /usr/local/bin/clusterctl + KUBECONFIG= clusterctl init --core cluster-api:v1.9.6 --bootstrap canonical-kubernetes --control-plane canonical-kubernetes --infrastructure openstack:v0.11.3 --addon helm + + +Also Kubernetes cluster credentials should be passed as a juju secret to the +magnum charm via config option `kubeconfig` +Steps to create juju secret and update config + + juju add-secret secret-kubeconfig kubeconfig#file= + juju grant-secret secret-kubeconfig magnum + juju config magnum kubeconfig= + ## Relations magnum-k8s requires the following relations: diff --git a/charms/magnum-k8s/charmcraft.yaml b/charms/magnum-k8s/charmcraft.yaml index 221c414e..e2d77706 100644 --- a/charms/magnum-k8s/charmcraft.yaml +++ b/charms/magnum-k8s/charmcraft.yaml @@ -32,6 +32,11 @@ config: default: RegionOne description: Name of the OpenStack region type: string + kubeconfig: + type: secret + description: | + Kubeconfig to connect to Cluster API management cluster. + The value should be juju secret. containers: magnum-api: diff --git a/charms/magnum-k8s/src/charm.py b/charms/magnum-k8s/src/charm.py index d1390aaa..281e4f8c 100755 --- a/charms/magnum-k8s/src/charm.py +++ b/charms/magnum-k8s/src/charm.py @@ -18,6 +18,9 @@ This charm provide Magnum services as part of an OpenStack deployment """ import logging +from functools import ( + cached_property, +) from typing import ( TYPE_CHECKING, List, @@ -28,11 +31,17 @@ import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as sunbeam_config_contexts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers import ops_sunbeam.tracing as sunbeam_tracing +import yaml from ops.framework import ( StoredState, ) +from ops.model import ( + ModelError, + SecretNotFoundError, +) logger = logging.getLogger(__name__) @@ -51,7 +60,7 @@ class MagnumConfigurationContext(sunbeam_config_contexts.ConfigContext): @property def ready(self) -> bool: """Whether the context has all the data is needs.""" - return self.charm.user_id_ops.ready + return self.charm.user_id_ops.ready and bool(self.charm.kubeconfig) def context(self) -> dict: """Magnum configuration context.""" @@ -63,6 +72,7 @@ class MagnumConfigurationContext(sunbeam_config_contexts.ConfigContext): "domain_name": self.charm.domain_name, "domain_admin_user": username, "domain_admin_password": password, + "kubeconfig": self.charm.kubeconfig or "", } @@ -121,6 +131,11 @@ class MagnumConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): "magnum", 0o640, ), + sunbeam_core.ContainerConfigFile( + "/etc/magnum/kubeconfig", + "magnum", + "magnum", + ), ] @property @@ -271,6 +286,20 @@ class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): ) return pebble_handlers + def configure_containers(self) -> None: + """Configure containers on this unit.""" + if not self.config.get("kubeconfig"): + raise sunbeam_guard.BlockedExceptionError( + "Configuration parameter kubeconfig not set" + ) + + if self.kubeconfig is None: + raise sunbeam_guard.BlockedExceptionError( + "Error in retrieving kubeconfig" + ) + + super().configure_containers() + @property def domain_name(self) -> str: """Domain name to create.""" @@ -281,6 +310,21 @@ class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """User to manage users and projects in domain_name.""" return "magnum_domain_admin" + @cached_property + def kubeconfig(self) -> str | None: + """Kubeconfig content to connect to k8s management cluster.""" + try: + kubeconfig_secret = self.model.get_secret( + id=self.config.get("kubeconfig") + ) + kubeconfig_secret_content = kubeconfig_secret.get_content() + kubeconfig_string = kubeconfig_secret_content.get("kubeconfig") + kubeconfig = yaml.safe_load(kubeconfig_string) + return yaml.dump(kubeconfig) + except (SecretNotFoundError, ModelError, yaml.YAMLError) as e: + logger.info(f"Error in retrieving kubeconfig secret: {e}") + return None + def _get_create_role_ops(self) -> list: """Generate ops request for create role.""" return [ diff --git a/charms/magnum-k8s/src/templates/kubeconfig.j2 b/charms/magnum-k8s/src/templates/kubeconfig.j2 new file mode 100644 index 00000000..0e95fbfe --- /dev/null +++ b/charms/magnum-k8s/src/templates/kubeconfig.j2 @@ -0,0 +1 @@ +{{ magnum.kubeconfig }} diff --git a/charms/magnum-k8s/src/templates/magnum.conf.j2 b/charms/magnum-k8s/src/templates/magnum.conf.j2 index 5b489814..64590617 100644 --- a/charms/magnum-k8s/src/templates/magnum.conf.j2 +++ b/charms/magnum-k8s/src/templates/magnum.conf.j2 @@ -67,3 +67,15 @@ ca_file = /usr/local/share/ca-certificates/ca-bundle.pem [audit_middleware_notifications] driver = log + +[cluster_template] +kubernetes_allowed_network_drivers = cilium + +[capi_helm] +kubeconfig_file = /etc/magnum/kubeconfig +# Empty repo so that helm chart can be downloaded from OCI registry +helm_chart_repo = "" +helm_chart_name = oci://ghcr.io/canonical/charts/openstack-ck8s-cluster +default_helm_chart_version = 0.1.0 +api_resources = {"K8sControlPlane": {"api_version": "controlplane.cluster.x-k8s.io/v1beta2", "plural_name": "ck8scontrolplanes"}, "OpenstackCluster": {"api_version": "infrastructure.cluster.x-k8s.io/v1beta1"}} +k8s_control_plane_resource_conditions = MachinesReady,Ready,ControlPlaneComponentsHealthy diff --git a/charms/magnum-k8s/tests/unit/test_magnum_charm.py b/charms/magnum-k8s/tests/unit/test_magnum_charm.py index 4c9832d4..1a6489b8 100644 --- a/charms/magnum-k8s/tests/unit/test_magnum_charm.py +++ b/charms/magnum-k8s/tests/unit/test_magnum_charm.py @@ -23,6 +23,7 @@ from unittest.mock import ( import charm import ops_sunbeam.test_utils as test_utils +import yaml from ops.testing import ( Harness, ) @@ -79,6 +80,13 @@ class TestMagnumOperatorCharm(test_utils.CharmTestCase): self.addCleanup(self.harness.cleanup) self.harness.begin() + # Create a secret for kubeconfig and update the charm config + secret_id = self.harness.add_model_secret( + self.harness.charm.app.name, + {"kubeconfig": yaml.dump({"cluster": "testcluster"})}, + ) + self.harness.update_config({"kubeconfig": secret_id}) + def add_complete_identity_resource_relation(self, harness: Harness) -> int: """Add complete Identity resource relation.""" rel_id = harness.add_relation("identity-ops", "keystone") @@ -103,9 +111,11 @@ class TestMagnumOperatorCharm(test_utils.CharmTestCase): def test_pebble_ready_handler(self): """Test pebble ready handler.""" - self.assertEqual(self.harness.charm.seen_events, []) + self.assertEqual( + self.harness.charm.seen_events, ["ConfigChangedEvent"] + ) test_utils.set_all_pebbles_ready(self.harness) - self.assertEqual(len(self.harness.charm.seen_events), 2) + self.assertEqual(len(self.harness.charm.seen_events), 3) def test_all_relations(self): """Test all integrations for operator.""" diff --git a/tests/all-k8s/tests.yaml b/tests/all-k8s/tests.yaml index 8e8b4f68..8b427270 100644 --- a/tests/all-k8s/tests.yaml +++ b/tests/all-k8s/tests.yaml @@ -4,6 +4,7 @@ smoke_bundles: - smoke configure: - zaza.sunbeam.charm_tests.k8s.setup.add_loadbalancer_annotations + - zaza.sunbeam.charm_tests.magnum.setup.configure - zaza.sunbeam.charm_tests.keystone.setup.wait_for_all_endpoints - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles - zaza.openstack.charm_tests.nova.setup.create_flavors @@ -100,8 +101,8 @@ target_deploy_status: workload-status: active workload-status-message-regex: '^$' magnum: - workload-status: active - workload-status-message-regex: '^$' + workload-status: blocked + workload-status-message-regex: '^.*Configuration parameter kubeconfig not set$' manila: workload-status: active workload-status-message-regex: '^$' diff --git a/tests/local/zaza/sunbeam/charm_tests/magnum/setup.py b/tests/local/zaza/sunbeam/charm_tests/magnum/setup.py new file mode 100644 index 00000000..ca684254 --- /dev/null +++ b/tests/local/zaza/sunbeam/charm_tests/magnum/setup.py @@ -0,0 +1,61 @@ +# Copyright (c) 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re + +import jubilant +import zaza.model + + +def configure(): + """Setup any configurations required by Magnum. + + Setup kubeconfig configuration parameter by adding a juju secret. + """ + model = zaza.model.get_juju_model() + application = "magnum" + secret_name = "kubeconfig" + secret_content = {"kubeconfig": "fake-kubeconfig"} + secret_not_found_pattern = r'ERROR secret ".*" not found' + secret_uri: jubilant.secrettypes.SecretURI + + logging.debug(f"Magnum configure: Using model {model}") + juju = jubilant.Juju(model=model) + + create_secret = False + try: + kubeconfig_secret = juju.show_secret(identifier=secret_name) + secret_uri = kubeconfig_secret.uri + logging.debug(f"Juju secret {secret_name} found") + except jubilant.CLIError as e: + match = re.search(secret_not_found_pattern, e.stderr) + if not match: + raise + + create_secret = True + + if create_secret: + logging.debug(f"Create juju secret {secret_name}") + secret_uri = juju.add_secret(name=secret_name, content=secret_content) + juju.grant_secret(secret_uri, application) + + logging.info(f"Setting {application} kubeconfig option") + juju.config(app=application, values={"kubeconfig": secret_uri}) + logging.info(f"Waiting for application {application} to be active") + juju.wait( + lambda status: jubilant.all_active(status, application), + timeout=180, + ) diff --git a/tox.ini b/tox.ini index f9466b50..8e6204cb 100644 --- a/tox.ini +++ b/tox.ini @@ -95,6 +95,7 @@ deps = git+https://github.com/openstack-charmers/zaza.git#egg=zaza git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack git+https://opendev.org/openstack/tempest.git#egg=tempest + git+https://github.com/canonical/jubilant.git@v1.3.0#egg=jubilant # Pin httpx version due to bug https://github.com/gtsystem/lightkube/issues/78 httpx>=0.24.0,<0.28.0 lightkube