Add functional tests and support config flags

This commit is contained in:
Liam Young 2023-09-24 11:00:16 +00:00
parent f64121c61f
commit 7deda62ef5
11 changed files with 287 additions and 152 deletions

View File

@ -0,0 +1,3 @@
[DEFAULT]
test_path=./tests/unit
top_dir=./tests

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
../config.yaml

View File

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

View File

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

View File

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