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
|
display action descriptions run `juju actions magnum`. If the charm is not
|
||||||
deployed then see file `actions.yaml`.
|
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
|
## Relations
|
||||||
|
|
||||||
magnum-k8s requires the following relations:
|
magnum-k8s requires the following relations:
|
||||||
|
@@ -32,6 +32,11 @@ config:
|
|||||||
default: RegionOne
|
default: RegionOne
|
||||||
description: Name of the OpenStack region
|
description: Name of the OpenStack region
|
||||||
type: string
|
type: string
|
||||||
|
kubeconfig:
|
||||||
|
type: secret
|
||||||
|
description: |
|
||||||
|
Kubeconfig to connect to Cluster API management cluster.
|
||||||
|
The value should be juju secret.
|
||||||
|
|
||||||
containers:
|
containers:
|
||||||
magnum-api:
|
magnum-api:
|
||||||
|
@@ -18,6 +18,9 @@ This charm provide Magnum services as part of an OpenStack deployment
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from functools import (
|
||||||
|
cached_property,
|
||||||
|
)
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
List,
|
List,
|
||||||
@@ -28,11 +31,17 @@ import ops_sunbeam.charm as sunbeam_charm
|
|||||||
import ops_sunbeam.config_contexts as sunbeam_config_contexts
|
import ops_sunbeam.config_contexts as sunbeam_config_contexts
|
||||||
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
||||||
import ops_sunbeam.core as sunbeam_core
|
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.relation_handlers as sunbeam_rhandlers
|
||||||
import ops_sunbeam.tracing as sunbeam_tracing
|
import ops_sunbeam.tracing as sunbeam_tracing
|
||||||
|
import yaml
|
||||||
from ops.framework import (
|
from ops.framework import (
|
||||||
StoredState,
|
StoredState,
|
||||||
)
|
)
|
||||||
|
from ops.model import (
|
||||||
|
ModelError,
|
||||||
|
SecretNotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -51,7 +60,7 @@ class MagnumConfigurationContext(sunbeam_config_contexts.ConfigContext):
|
|||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
"""Whether the context has all the data is needs."""
|
"""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:
|
def context(self) -> dict:
|
||||||
"""Magnum configuration context."""
|
"""Magnum configuration context."""
|
||||||
@@ -63,6 +72,7 @@ class MagnumConfigurationContext(sunbeam_config_contexts.ConfigContext):
|
|||||||
"domain_name": self.charm.domain_name,
|
"domain_name": self.charm.domain_name,
|
||||||
"domain_admin_user": username,
|
"domain_admin_user": username,
|
||||||
"domain_admin_password": password,
|
"domain_admin_password": password,
|
||||||
|
"kubeconfig": self.charm.kubeconfig or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +131,11 @@ class MagnumConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
|||||||
"magnum",
|
"magnum",
|
||||||
0o640,
|
0o640,
|
||||||
),
|
),
|
||||||
|
sunbeam_core.ContainerConfigFile(
|
||||||
|
"/etc/magnum/kubeconfig",
|
||||||
|
"magnum",
|
||||||
|
"magnum",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -271,6 +286,20 @@ class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
)
|
)
|
||||||
return pebble_handlers
|
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
|
@property
|
||||||
def domain_name(self) -> str:
|
def domain_name(self) -> str:
|
||||||
"""Domain name to create."""
|
"""Domain name to create."""
|
||||||
@@ -281,6 +310,21 @@ class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
"""User to manage users and projects in domain_name."""
|
"""User to manage users and projects in domain_name."""
|
||||||
return "magnum_domain_admin"
|
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:
|
def _get_create_role_ops(self) -> list:
|
||||||
"""Generate ops request for create role."""
|
"""Generate ops request for create role."""
|
||||||
return [
|
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]
|
[audit_middleware_notifications]
|
||||||
driver = log
|
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 charm
|
||||||
import ops_sunbeam.test_utils as test_utils
|
import ops_sunbeam.test_utils as test_utils
|
||||||
|
import yaml
|
||||||
from ops.testing import (
|
from ops.testing import (
|
||||||
Harness,
|
Harness,
|
||||||
)
|
)
|
||||||
@@ -79,6 +80,13 @@ class TestMagnumOperatorCharm(test_utils.CharmTestCase):
|
|||||||
self.addCleanup(self.harness.cleanup)
|
self.addCleanup(self.harness.cleanup)
|
||||||
self.harness.begin()
|
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:
|
def add_complete_identity_resource_relation(self, harness: Harness) -> int:
|
||||||
"""Add complete Identity resource relation."""
|
"""Add complete Identity resource relation."""
|
||||||
rel_id = harness.add_relation("identity-ops", "keystone")
|
rel_id = harness.add_relation("identity-ops", "keystone")
|
||||||
@@ -103,9 +111,11 @@ class TestMagnumOperatorCharm(test_utils.CharmTestCase):
|
|||||||
|
|
||||||
def test_pebble_ready_handler(self):
|
def test_pebble_ready_handler(self):
|
||||||
"""Test pebble ready handler."""
|
"""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)
|
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):
|
def test_all_relations(self):
|
||||||
"""Test all integrations for operator."""
|
"""Test all integrations for operator."""
|
||||||
|
@@ -4,6 +4,7 @@ smoke_bundles:
|
|||||||
- smoke
|
- smoke
|
||||||
configure:
|
configure:
|
||||||
- zaza.sunbeam.charm_tests.k8s.setup.add_loadbalancer_annotations
|
- 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.sunbeam.charm_tests.keystone.setup.wait_for_all_endpoints
|
||||||
- zaza.openstack.charm_tests.keystone.setup.add_tempest_roles
|
- zaza.openstack.charm_tests.keystone.setup.add_tempest_roles
|
||||||
- zaza.openstack.charm_tests.nova.setup.create_flavors
|
- zaza.openstack.charm_tests.nova.setup.create_flavors
|
||||||
@@ -100,8 +101,8 @@ target_deploy_status:
|
|||||||
workload-status: active
|
workload-status: active
|
||||||
workload-status-message-regex: '^$'
|
workload-status-message-regex: '^$'
|
||||||
magnum:
|
magnum:
|
||||||
workload-status: active
|
workload-status: blocked
|
||||||
workload-status-message-regex: '^$'
|
workload-status-message-regex: '^.*Configuration parameter kubeconfig not set$'
|
||||||
manila:
|
manila:
|
||||||
workload-status: active
|
workload-status: active
|
||||||
workload-status-message-regex: '^$'
|
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.git#egg=zaza
|
||||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
||||||
git+https://opendev.org/openstack/tempest.git#egg=tempest
|
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
|
# Pin httpx version due to bug https://github.com/gtsystem/lightkube/issues/78
|
||||||
httpx>=0.24.0,<0.28.0
|
httpx>=0.24.0,<0.28.0
|
||||||
lightkube
|
lightkube
|
||||||
|
Reference in New Issue
Block a user