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 <hemanth.nakkina@canonical.com>
This commit is contained in:
Hemanth Nakkina
2025-08-19 17:27:42 +05:30
parent d9f95d07b1
commit 75c50a1e18
9 changed files with 161 additions and 5 deletions

View File

@@ -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=<kubeconfig file path> 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=<kubeconfig file path>
juju grant-secret secret-kubeconfig magnum
juju config magnum kubeconfig=<secret-kubeconfig URI>
## Relations
magnum-k8s requires the following relations:

View File

@@ -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:

View File

@@ -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 [

View File

@@ -0,0 +1 @@
{{ magnum.kubeconfig }}

View File

@@ -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

View File

@@ -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."""

View File

@@ -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: '^$'

View File

@@ -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,
)

View File

@@ -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