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:
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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 [
|
||||
|
1
charms/magnum-k8s/src/templates/kubeconfig.j2
Normal file
1
charms/magnum-k8s/src/templates/kubeconfig.j2
Normal file
@@ -0,0 +1 @@
|
||||
{{ magnum.kubeconfig }}
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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: '^$'
|
||||
|
61
tests/local/zaza/sunbeam/charm_tests/magnum/setup.py
Normal file
61
tests/local/zaza/sunbeam/charm_tests/magnum/setup.py
Normal 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,
|
||||
)
|
1
tox.ini
1
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
|
||||
|
Reference in New Issue
Block a user