diff --git a/charms/keystone-ldap-k8s/.stestr.conf b/charms/keystone-ldap-k8s/.stestr.conf new file mode 100644 index 00000000..e4750de4 --- /dev/null +++ b/charms/keystone-ldap-k8s/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./tests/unit +top_dir=./tests diff --git a/charms/keystone-ldap-k8s/config.yaml b/charms/keystone-ldap-k8s/config.yaml index 37190374..a8599027 100644 --- a/charms/keystone-ldap-k8s/config.yaml +++ b/charms/keystone-ldap-k8s/config.yaml @@ -44,9 +44,7 @@ options: default: description: | Additional LDAP configuration options. - For simple configurations use a comma separated string of key=value pairs. - "user_allow_create=False, user_allow_update=False, user_allow_delete=False" - For more complex configurations use a json like string with double quotes + Use a json like string with double quotes and braces around all the options and single quotes around complex values. "{user_tree_dn: 'DC=dc1,DC=ad,DC=example,DC=com', user_allow_create: False, diff --git a/charms/keystone-ldap-k8s/src/charm.py b/charms/keystone-ldap-k8s/src/charm.py index 073c927d..b2807cc4 100755 --- a/charms/keystone-ldap-k8s/src/charm.py +++ b/charms/keystone-ldap-k8s/src/charm.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2021 Canonical Ltd. +# Copyright 2023 Canonical Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,46 +17,91 @@ # # Learn more at: https://juju.is/docs/sdk -"""Charm the service. +"""Keystone LDAP configuration. -Refer to the following post for a quick-start guide that will help you -develop a new k8s charm using the Operator Framework: - - https://discourse.charmhub.io/t/4208 +Send domain configuration to the keystone charm. """ -import jinja2 +import json import logging -from typing import ( - Callable, - List, - Mapping, -) +from typing import Callable, List, Mapping +import charms.keystone_ldap_k8s.v0.domain_config as sunbeam_dc_svc +import jinja2 import ops.charm +import ops_sunbeam.charm as sunbeam_charm +import ops_sunbeam.config_contexts as config_contexts +import ops_sunbeam.relation_handlers as sunbeam_rhandlers from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, WaitingStatus # Log messages can be retrieved using juju debug-log logger = logging.getLogger(__name__) -VALID_LOG_LEVELS = ["info", "debug", "warning", "error", "critical"] -import ops_sunbeam.charm as sunbeam_charm -import ops_sunbeam.relation_handlers as sunbeam_rhandlers -import charms.keystone_ldap_k8s.v0.domain_config as sunbeam_dc_svc -import ops_sunbeam.config_contexts as config_contexts -import json +LDAP_OPTINONS = [ + "server", + "user", + "password", + "suffix", + "readonly", + "query_scope", + "user_tree_dn", + "user_filter", + "user_objectclass", + "user_id_attribute", + "user_name_attribute", + "user_enabled_attribute", + "user_enabled_invert", + "user_enabled_mask", + "user_enabled_default", + "user_enabled_emulation", + "user_enabled_emulation_dn", + "group_tree_dn", + "group_objectclass", + "group_id_attribute", + "group_name_attribute", + "group_member_attribute", + "group_members_are_ids", + "use_pool", + "pool_size", + "pool_retry_max", + "pool_connection_timeout", +] -class LDAPConfigFlagsContext(config_contexts.ConfigContext): + +class LDAPConfigContext(config_contexts.ConfigContext): """Configuration context for cinder parameters.""" def context(self) -> dict: """Generate context information for cinder config.""" + # LDAP config follows the patterns that if a user has + # explicitly set a value then it should be rendered + # otherwise the option is omitted. This is slighttly + # complicated by the fact that the model.config does + # not include settings that have not been set. + context = {} config_flags = {} config = self.charm.model.config.get + for option in LDAP_OPTINONS: + config_option = "ldap-" + option.replace("_", "-") + config_value = config(config_option) + if config_value is not None and config_value != "": + context[option] = config_value raw_config_flags = config("ldap-config-flags") if raw_config_flags: config_flags = json.loads(raw_config_flags) - return {'flags': config_flags} + for key, value in config_flags.items(): + if key in context.keys(): + logger.warning( + "Ignoring {} passed via ldap-config-flags, please use charm config to manage this setting".format( + key + ) + ) + else: + context[key] = value + if context.get("server"): + # Should probably change the config.yaml rather than having to + # rename the key + context["url"] = context.pop("server") + return {"config": context} class DomainConfigProvidesHandler(sunbeam_rhandlers.RelationHandler): @@ -95,15 +140,13 @@ class DomainConfigProvidesHandler(sunbeam_rhandlers.RelationHandler): class KeystoneLDAPK8SCharm(sunbeam_charm.OSBaseOperatorCharm): """Charm the service.""" + DOMAIN_CONFIG_RELATION_NAME = "domain-config" def __init__(self, *args): super().__init__(*args) - self.send_domain_config() - def get_relation_handlers( - self, handlers=None - ) -> List[sunbeam_rhandlers.RelationHandler]: + def get_relation_handlers(self, handlers=None) -> List[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] if self.can_add_handler(self.DOMAIN_CONFIG_RELATION_NAME, handlers): @@ -115,37 +158,40 @@ class KeystoneLDAPK8SCharm(sunbeam_charm.OSBaseOperatorCharm): handlers.append(self.dc_handler) return super().get_relation_handlers(handlers) - @property def config_contexts(self) -> List[config_contexts.ConfigContext]: """Configuration contexts for the operator.""" contexts = super().config_contexts - contexts.append(LDAPConfigFlagsContext(self, "ldap_config_flags")) + contexts.append(LDAPConfigContext(self, "ldap_config")) return contexts - - def send_domain_config(self, event=None): - try: - domain_name = self.config['domain-name'] + def send_domain_config(self, event=None) -> None: + """Send domain configuration to keystone.""" + try: + domain_name = self.config["domain-name"] except KeyError: return loader = jinja2.FileSystemLoader(self.template_dir) _tmpl_env = jinja2.Environment(loader=loader) template = _tmpl_env.get_template("keystone.conf") self.dc_handler.domain_config.set_domain_info( - domain_name=domain_name, - config_contents=template.render(self.contexts())) + domain_name=domain_name, config_contents=template.render(self.contexts()) + ) - def configure_app_leader(self, event): + def configure_app_leader(self, event) -> None: + """Configure application.""" self.send_domain_config() self.set_leader_ready() @property def databases(self) -> Mapping[str, str]: + """Config charm has no databases.""" return {} def get_pebble_handlers(self): + """Config charm has no containers.""" return [] + if __name__ == "__main__": # pragma: nocover main(KeystoneLDAPK8SCharm) diff --git a/charms/keystone-ldap-k8s/src/templates/keystone.conf b/charms/keystone-ldap-k8s/src/templates/keystone.conf index 5e70827c..f41266d3 100644 --- a/charms/keystone-ldap-k8s/src/templates/keystone.conf +++ b/charms/keystone-ldap-k8s/src/templates/keystone.conf @@ -1,120 +1,7 @@ [ldap] -url = {{ options.ldap_server }} -{% if options.ldap_user and options.ldap_password -%} -user = {{ options.ldap_user }} -password = {{ options.ldap_password }} -{% endif -%} -suffix = {{ options.ldap_suffix }} - -user_allow_create = {{ not options.ldap_readonly }} -user_allow_update = {{ not options.ldap_readonly }} -user_allow_delete = {{ not options.ldap_readonly }} - -group_allow_create = {{ not options.ldap_readonly }} -group_allow_update = {{ not options.ldap_readonly }} -group_allow_delete = {{ not options.ldap_readonly }} - -{% if options.tls_ca_ldap -%} -use_tls = {{ options.use_tls }} -tls_req_cert = demand -tls_cacertfile = {{ options.backend_ca_file }} -{% endif -%} - -{% if options.ldap_query_scope -%} -query_scope = {{ options.ldap_query_scope }} -{% endif -%} - -{% if options.ldap_user_tree_dn -%} -user_tree_dn = {{ options.ldap_user_tree_dn }} -{% endif -%} - -{% if options.ldap_user_filter -%} -user_filter = {{ options.ldap_user_filter }} -{% endif -%} - -{% if options.ldap_user_objectclass -%} -user_objectclass = {{ options.ldap_user_objectclass }} -{% endif -%} - -{% if options.ldap_user_id_attribute -%} -user_id_attribute = {{ options.ldap_user_id_attribute }} -{% endif -%} - -{% if options.ldap_user_name_attribute -%} -user_name_attribute = {{ options.ldap_user_name_attribute }} -{% endif -%} - -{% if options.ldap_user_enabled_attribute -%} -user_enabled_attribute = {{ options.ldap_user_enabled_attribute }} -{% endif -%} - -{% if options.ldap_user_enabled_invert|length -%} -user_enabled_invert = {{ options.ldap_user_enabled_invert }} -{% endif -%} - -{% if options.ldap_user_enabled_mask|length -%} -user_enabled_mask = {{ options.ldap_user_enabled_mask }} -{% endif -%} - -{% if options.ldap_user_enabled_default -%} -user_enabled_default = {{ options.ldap_user_enabled_default }} -{% endif -%} - -{% if options.ldap_user_enabled_emulation|length -%} -user_enabled_emulation = {{ options.ldap_user_enabled_emulation }} -{% endif -%} - -{% if options.ldap_user_enabled_emulation_dn -%} -user_enabled_emulation_dn = {{ options.ldap_user_enabled_emulation_dn }} -{% endif -%} - -{% if options.ldap_group_tree_dn -%} -group_tree_dn = {{ options.ldap_group_tree_dn }} -{% endif -%} - -{% if options.ldap_group_objectclass -%} -group_objectclass = {{ options.ldap_group_objectclass }} -{% endif -%} - -{% if options.ldap_group_id_attribute -%} -group_id_attribute = {{ options.ldap_group_id_attribute }} -{% endif -%} - -{% if options.ldap_group_name_attribute -%} -group_name_attribute = {{ options.ldap_group_name_attribute }} -{% endif -%} - -{% if options.ldap_group_member_attribute -%} -group_member_attribute = {{ options.ldap_group_member_attribute }} -{% endif -%} - -{% if options.ldap_group_members_are_ids|length -%} -group_members_are_ids = {{ options.ldap_group_members_are_ids }} -{% endif -%} - -{% if options.ldap_use_pool|length -%} -use_pool = {{ options.ldap_use_pool }} -{% endif -%} - -{% if options.ldap_pool_size|length -%} -pool_size = {{ options.ldap_pool_size }} -{% endif -%} - -{% if options.ldap_pool_retry_max|length -%} -pool_retry_max = {{ options.ldap_pool_retry_max }} -{% endif -%} - -{% if options.ldap_pool_connection_timeout|length -%} -pool_connection_timeout = {{ options.ldap_pool_connection_timeout }} -{% endif -%} - -# User supplied configuration flags -{% if ldap_config_flags.flags -%} -{% for key, value in ldap_config_flags.flags.items() -%} +{% for key, value in ldap_config.config.items()|sort -%} {{ key }} = {{ value }} {% endfor -%} -{% endif -%} [identity] driver = ldap - diff --git a/charms/keystone-ldap-k8s/test-requirements.txt b/charms/keystone-ldap-k8s/test-requirements.txt index 23e005bd..49c26d40 100644 --- a/charms/keystone-ldap-k8s/test-requirements.txt +++ b/charms/keystone-ldap-k8s/test-requirements.txt @@ -10,7 +10,8 @@ mock flake8 stestr git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#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://github.com/gnuoy/zaza-openstack-tests.git@keystone-ldap-k8s#egg=zaza.openstack git+https://opendev.org/openstack/tempest.git#egg=tempest ops # Subunit 1.4.3+ requires extras diff --git a/charms/keystone-ldap-k8s/tests/__init__.py b/charms/keystone-ldap-k8s/tests/__init__.py new file mode 100644 index 00000000..328ff1df --- /dev/null +++ b/charms/keystone-ldap-k8s/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 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. + +"""Tests for charm.""" diff --git a/charms/keystone-ldap-k8s/tests/bundles/smoke.yaml b/charms/keystone-ldap-k8s/tests/bundles/smoke.yaml new file mode 100644 index 00000000..2390e50d --- /dev/null +++ b/charms/keystone-ldap-k8s/tests/bundles/smoke.yaml @@ -0,0 +1,75 @@ +bundle: kubernetes +applications: + + mysql: + charm: ch:mysql-k8s + channel: 8.0/stable + scale: 1 + trust: false + + # Currently traefik is required for networking things. + # If this isn't present, the units will hang at "installing agent". + traefik: + charm: ch:traefik-k8s + channel: 1.0/stable + scale: 1 + trust: true + + # required for glance + rabbitmq: + charm: ch:rabbitmq-k8s + channel: 3.9/edge + scale: 1 + trust: true + + keystone: + charm: ch:keystone-k8s + channel: 2023.1/edge/gnuoy + series: jammy + scale: 1 + trust: true + options: + admin-role: admin + storage: + fernet-keys: 5M + credential-keys: 5M + + keystone-ldap: + charm: ../../keystone-ldap-k8s.charm + scale: 1 + + ldap-server: + charm: ch:ldap-test-fixture-k8s + channel: edge + scale: 1 + + glance: + charm: ch:glance-k8s + channel: 2023.1/edge + scale: 1 + trust: true + storage: + local-repository: 5G + +relations: +- - keystone:identity-service + - glance:identity-service +- - rabbitmq:amqp + - glance:amqp + +- - traefik:ingress + - keystone:ingress-public +- - traefik:ingress + - glance:ingress-public + +- - rabbitmq:amqp + - keystone:amqp + +- - mysql:database + - keystone:database + +- - mysql:database + - glance:database + +- - keystone:domain-config + - keystone-ldap:domain-config diff --git a/charms/keystone-ldap-k8s/tests/config.yaml b/charms/keystone-ldap-k8s/tests/config.yaml new file mode 120000 index 00000000..e84e89a8 --- /dev/null +++ b/charms/keystone-ldap-k8s/tests/config.yaml @@ -0,0 +1 @@ +../config.yaml \ No newline at end of file diff --git a/charms/keystone-ldap-k8s/tests/tests.yaml b/charms/keystone-ldap-k8s/tests/tests.yaml new file mode 100644 index 00000000..87decc46 --- /dev/null +++ b/charms/keystone-ldap-k8s/tests/tests.yaml @@ -0,0 +1,49 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.openstack.charm_tests.keystone.setup.wait_for_all_endpoints + - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles +tests: + - zaza.openstack.charm_tests.keystone.tests_ldap_k8s.LdapExplicitCharmConfigTestsK8S + - zaza.openstack.charm_tests.keystone.tests.KeystoneTempestTestK8S +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + exclude-list: + - "tempest.api.image.v2.test_images.BasicOperationsImagesTest.test_register_upload_get_image_file" + include-list: + - "tempest.api.identity.v3.test_application_credentials.ApplicationCredentialsV3Test.test_create_application_credential" + +target_deploy_status: + traefik: + workload-status: active + workload-status-message-regex: '^$' + traefik-public: + workload-status: active + workload-status-message-regex: '^$' + rabbitmq: + workload-status: active + workload-status-message-regex: '^$' + glance: + workload-status: active + workload-status-message-regex: '^$' + keystone: + workload-status: waiting + workload-status-message-regex: '^.*domain-config.*integration incomplete.*$|^$' + keystone-ldap: + workload-status: active + workload-status-message-regex: '^$' + ldap-server: + workload-status: active + workload-status-message-regex: '^$' + mysql: + workload-status: active + workload-status-message-regex: '^.*$' diff --git a/charms/keystone-ldap-k8s/tests/unit/__init__.py b/charms/keystone-ldap-k8s/tests/unit/__init__.py new file mode 100644 index 00000000..304f420a --- /dev/null +++ b/charms/keystone-ldap-k8s/tests/unit/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 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. + +"""Unit tests for charm.""" diff --git a/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py new file mode 100644 index 00000000..0f05bbaa --- /dev/null +++ b/charms/keystone-ldap-k8s/tests/unit/test_keystone_ldap_charm.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +# Copyright 2021 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. + +"""Define keystone tests.""" + +import json +import os +from unittest.mock import ANY, MagicMock + +import mock +import ops_sunbeam.test_utils as test_utils + +import charm + + +class _KeystoneLDAPK8SCharm(charm.KeystoneLDAPK8SCharm): + """Create Keystone operator test charm.""" + + def __init__(self, framework): + self.seen_events = [] + super().__init__(framework) + + def _log_event(self, event): + self.seen_events.append(type(event).__name__) + + def configure_charm(self, event): + super().configure_charm(event) + self._log_event(event) + + @property + def public_ingress_address(self) -> str: + return "10.0.0.10"