From 36c87727e3d57f3f55822a9b90c5fb5dfac442a4 Mon Sep 17 00:00:00 2001 From: David Garcia Date: Mon, 30 Aug 2021 18:01:22 +0200 Subject: [PATCH] Add Kafka and Zookeeper charms in operator framework Change-Id: I15645825ab8ff927ad0f72bbfd53ea71343b2be4 Signed-off-by: David Garcia --- installers/charm/kafka/.gitignore | 28 ++ installers/charm/kafka/.jujuignore | 32 +++ installers/charm/kafka/.yamllint.yaml | 32 +++ installers/charm/kafka/README.md | 22 ++ installers/charm/kafka/charmcraft.yaml | 37 +++ installers/charm/kafka/config.yaml | 32 +++ installers/charm/kafka/metadata.yaml | 53 ++++ installers/charm/kafka/requirements-test.txt | 21 ++ installers/charm/kafka/requirements.txt | 22 ++ installers/charm/kafka/src/charm.py | 239 ++++++++++++++++++ installers/charm/kafka/tests/__init__.py | 40 +++ installers/charm/kafka/tests/test_charm.py | 77 ++++++ installers/charm/kafka/tox.ini | 126 +++++++++ installers/charm/zookeeper/.gitignore | 28 ++ installers/charm/zookeeper/.jujuignore | 32 +++ installers/charm/zookeeper/.yamllint.yaml | 32 +++ installers/charm/zookeeper/README.md | 22 ++ installers/charm/zookeeper/charmcraft.yaml | 37 +++ installers/charm/zookeeper/config.yaml | 89 +++++++ installers/charm/zookeeper/metadata.yaml | 51 ++++ .../charm/zookeeper/requirements-test.txt | 21 ++ installers/charm/zookeeper/requirements.txt | 22 ++ installers/charm/zookeeper/src/charm.py | 181 +++++++++++++ installers/charm/zookeeper/tests/__init__.py | 40 +++ .../charm/zookeeper/tests/test_charm.py | 94 +++++++ installers/charm/zookeeper/tox.ini | 126 +++++++++ 26 files changed, 1536 insertions(+) create mode 100644 installers/charm/kafka/.gitignore create mode 100644 installers/charm/kafka/.jujuignore create mode 100644 installers/charm/kafka/.yamllint.yaml create mode 100644 installers/charm/kafka/README.md create mode 100644 installers/charm/kafka/charmcraft.yaml create mode 100644 installers/charm/kafka/config.yaml create mode 100644 installers/charm/kafka/metadata.yaml create mode 100644 installers/charm/kafka/requirements-test.txt create mode 100644 installers/charm/kafka/requirements.txt create mode 100755 installers/charm/kafka/src/charm.py create mode 100644 installers/charm/kafka/tests/__init__.py create mode 100644 installers/charm/kafka/tests/test_charm.py create mode 100644 installers/charm/kafka/tox.ini create mode 100644 installers/charm/zookeeper/.gitignore create mode 100644 installers/charm/zookeeper/.jujuignore create mode 100644 installers/charm/zookeeper/.yamllint.yaml create mode 100644 installers/charm/zookeeper/README.md create mode 100644 installers/charm/zookeeper/charmcraft.yaml create mode 100644 installers/charm/zookeeper/config.yaml create mode 100644 installers/charm/zookeeper/metadata.yaml create mode 100644 installers/charm/zookeeper/requirements-test.txt create mode 100644 installers/charm/zookeeper/requirements.txt create mode 100755 installers/charm/zookeeper/src/charm.py create mode 100644 installers/charm/zookeeper/tests/__init__.py create mode 100644 installers/charm/zookeeper/tests/test_charm.py create mode 100644 installers/charm/zookeeper/tox.ini diff --git a/installers/charm/kafka/.gitignore b/installers/charm/kafka/.gitignore new file mode 100644 index 00000000..a85ce6d9 --- /dev/null +++ b/installers/charm/kafka/.gitignore @@ -0,0 +1,28 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +venv +.vscode +*.charm +.coverage +coverage.xml +.stestr +cover \ No newline at end of file diff --git a/installers/charm/kafka/.jujuignore b/installers/charm/kafka/.jujuignore new file mode 100644 index 00000000..3738c1c2 --- /dev/null +++ b/installers/charm/kafka/.jujuignore @@ -0,0 +1,32 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +venv +.vscode +*.charm +.coverage +coverage.xml +.gitignore +.stestr +cover +tests/ +requirements* +tox.ini diff --git a/installers/charm/kafka/.yamllint.yaml b/installers/charm/kafka/.yamllint.yaml new file mode 100644 index 00000000..5244c94f --- /dev/null +++ b/installers/charm/kafka/.yamllint.yaml @@ -0,0 +1,32 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +--- +extends: default + +yaml-files: + - "*.yaml" + - "*.yml" + - ".yamllint" +ignore: | + .tox + cover/ + venv diff --git a/installers/charm/kafka/README.md b/installers/charm/kafka/README.md new file mode 100644 index 00000000..851adae9 --- /dev/null +++ b/installers/charm/kafka/README.md @@ -0,0 +1,22 @@ + + +# Kafka operator Charm for Kubernetes + diff --git a/installers/charm/kafka/charmcraft.yaml b/installers/charm/kafka/charmcraft.yaml new file mode 100644 index 00000000..0a285a9d --- /dev/null +++ b/installers/charm/kafka/charmcraft.yaml @@ -0,0 +1,37 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +type: charm +bases: + - build-on: + - name: ubuntu + channel: "20.04" + architectures: ["amd64"] + run-on: + - name: ubuntu + channel: "20.04" + architectures: + - amd64 + - aarch64 + - arm64 +parts: + charm: + build-packages: [git] diff --git a/installers/charm/kafka/config.yaml b/installers/charm/kafka/config.yaml new file mode 100644 index 00000000..4319a570 --- /dev/null +++ b/installers/charm/kafka/config.yaml @@ -0,0 +1,32 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +options: + image_pull_policy: + description: | + ImagePullPolicy configuration for the pod. + Possible values: always, ifnotpresent, never + type: string + default: always + num_partitions: + description: Kafka number of partitions per topic + type: int + default: 1 diff --git a/installers/charm/kafka/metadata.yaml b/installers/charm/kafka/metadata.yaml new file mode 100644 index 00000000..d1544068 --- /dev/null +++ b/installers/charm/kafka/metadata.yaml @@ -0,0 +1,53 @@ +# Copyright 2020 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +name: kafka +summary: "Kafka charm for Kubernetes." +description: | + A CAAS charm to deploy kafka. +series: + - kubernetes +tags: + - kubernetes + - osm + - kafka +min-juju-version: 2.8.0 +resources: + image: + type: oci-image + description: OSM docker image for kafka + upstream-source: "rocks.canonical.com:443/wurstmeister/kafka:2.12-2.2.1" +provides: + kafka: + interface: kafka +requires: + zookeeper: + interface: zookeeper +peers: + cluster: + interface: kafka-cluster +storage: + database: + type: filesystem + location: /var/lib/kafka +deployment: + type: stateful + service: cluster diff --git a/installers/charm/kafka/requirements-test.txt b/installers/charm/kafka/requirements-test.txt new file mode 100644 index 00000000..316f6d20 --- /dev/null +++ b/installers/charm/kafka/requirements-test.txt @@ -0,0 +1,21 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net + +mock==4.0.3 diff --git a/installers/charm/kafka/requirements.txt b/installers/charm/kafka/requirements.txt new file mode 100644 index 00000000..1a8928c7 --- /dev/null +++ b/installers/charm/kafka/requirements.txt @@ -0,0 +1,22 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master \ No newline at end of file diff --git a/installers/charm/kafka/src/charm.py b/installers/charm/kafka/src/charm.py new file mode 100755 index 00000000..763d4160 --- /dev/null +++ b/installers/charm/kafka/src/charm.py @@ -0,0 +1,239 @@ +#!/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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +# pylint: disable=E0213 + +import logging +from typing import NoReturn + + +from ops.framework import EventBase +from ops.main import main +from opslib.osm.charm import CharmedOsmBase, RelationsMissing +from opslib.osm.interfaces.kafka import KafkaCluster, KafkaServer +from opslib.osm.interfaces.zookeeper import ZookeeperClient +from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder +from opslib.osm.validator import ModelValidator, validator + +logger = logging.getLogger(__name__) + +KAFKA_PORT = 9092 +KAFKA_RESERVED_BROKER_MAX_ID = "999999999" + + +class ConfigModel(ModelValidator): + num_partitions: int + image_pull_policy: str + + @validator("image_pull_policy") + def validate_image_pull_policy(cls, v): + values = { + "always": "Always", + "ifnotpresent": "IfNotPresent", + "never": "Never", + } + v = v.lower() + if v not in values.keys(): + raise ValueError("value must be always, ifnotpresent or never") + return values[v] + + +class KafkaCharm(CharmedOsmBase): + """Kafka Charm.""" + + def __init__(self, *args) -> NoReturn: + """Kafka Charm constructor.""" + super().__init__(*args, oci_image="image") + self.kafka_cluster = KafkaCluster(self, "cluster") + self.kafka_server = KafkaServer(self, "kafka") + self.zookeeper_client = ZookeeperClient(self, "zookeeper") + event_observer_mapping = { + self.on["cluster"].relation_changed: self.configure_pod, + self.on["kafka"].relation_joined: self._publish_info, + self.on["zookeeper"].relation_changed: self.configure_pod, + self.on["zookeeper"].relation_broken: self.configure_pod, + } + for event, observer in event_observer_mapping.items(): + self.framework.observe(event, observer) + + @property + def num_units(self): + return self.kafka_cluster.num_units + + def _publish_info(self, event: EventBase): + """Publishes Kafka information. + + Args: + event (EventBase): Kafka relation event. + """ + if self.unit.is_leader(): + self.kafka_server.publish_info(self.app.name, KAFKA_PORT) + + def _check_missing_dependencies(self): + if self.zookeeper_client.is_missing_data_in_app(): + raise RelationsMissing(["zookeeper"]) + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + + # Check relations + self._check_missing_dependencies() + + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder() + + # Build Container + container_builder = ContainerV3Builder( + self.app.name, image_info, config.image_pull_policy + ) + + container_builder.add_port(name="kafka", port=KAFKA_PORT) + container_builder.add_tcpsocket_readiness_probe( + KAFKA_PORT, + initial_delay_seconds=10, + timeout_seconds=5, + period_seconds=5, + ) + container_builder.add_tcpsocket_liveness_probe( + KAFKA_PORT, + initial_delay_seconds=60, + timeout_seconds=10, + period_seconds=5, + ) + container_builder.add_envs( + { + "ENABLE_AUTO_EXTEND": "true", + "KAFKA_ADVERTISED_HOST_NAME": self.app.name, + "KAFKA_ADVERTISED_PORT": KAFKA_PORT, + "KAFKA_AUTO_CREATE_TOPICS_ENABLE": "true", + "KAFKA_RESERVED_BROKER_MAX_ID": KAFKA_RESERVED_BROKER_MAX_ID, + } + ) + container_builder.add_command( + [ + "sh", + "-c", + " ".join( + [ + "exec kafka-server-start.sh /opt/kafka/config/server.properties", + "--override broker.id=${HOSTNAME##*-}", + f"--override listeners=PLAINTEXT://:{KAFKA_PORT}", + f"--override zookeeper.connect={self.zookeeper_client.zookeeper_uri}", + "--override log.dir=/var/lib/kafka", + "--override auto.create.topics.enable=true", + "--override auto.leader.rebalance.enable=true", + "--override background.threads=10", + "--override compression.type=producer", + "--override delete.topic.enable=false", + "--override leader.imbalance.check.interval.seconds=300", + "--override leader.imbalance.per.broker.percentage=10", + "--override log.flush.interval.messages=9223372036854775807", + "--override log.flush.offset.checkpoint.interval.ms=60000", + "--override log.flush.scheduler.interval.ms=9223372036854775807", + "--override log.retention.bytes=-1", + "--override log.retention.hours=168", + "--override log.roll.hours=168", + "--override log.roll.jitter.hours=0", + "--override log.segment.bytes=1073741824", + "--override log.segment.delete.delay.ms=60000", + "--override message.max.bytes=1000012", + "--override min.insync.replicas=1", + "--override num.io.threads=8", + f"--override num.network.threads={self.num_units}", + "--override num.recovery.threads.per.data.dir=1", + "--override num.replica.fetchers=1", + "--override offset.metadata.max.bytes=4096", + "--override offsets.commit.required.acks=-1", + "--override offsets.commit.timeout.ms=5000", + "--override offsets.load.buffer.size=5242880", + "--override offsets.retention.check.interval.ms=600000", + "--override offsets.retention.minutes=1440", + "--override offsets.topic.compression.codec=0", + "--override offsets.topic.num.partitions=50", + f"--override offsets.topic.replication.factor={self.num_units}", + "--override offsets.topic.segment.bytes=104857600", + "--override queued.max.requests=500", + "--override quota.consumer.default=9223372036854775807", + "--override quota.producer.default=9223372036854775807", + "--override replica.fetch.min.bytes=1", + "--override replica.fetch.wait.max.ms=500", + "--override replica.high.watermark.checkpoint.interval.ms=5000", + "--override replica.lag.time.max.ms=10000", + "--override replica.socket.receive.buffer.bytes=65536", + "--override replica.socket.timeout.ms=30000", + "--override request.timeout.ms=30000", + "--override socket.receive.buffer.bytes=102400", + "--override socket.request.max.bytes=104857600", + "--override socket.send.buffer.bytes=102400", + "--override unclean.leader.election.enable=true", + "--override zookeeper.session.timeout.ms=6000", + "--override zookeeper.set.acl=false", + "--override broker.id.generation.enable=true", + "--override connections.max.idle.ms=600000", + "--override controlled.shutdown.enable=true", + "--override controlled.shutdown.max.retries=3", + "--override controlled.shutdown.retry.backoff.ms=5000", + "--override controller.socket.timeout.ms=30000", + "--override default.replication.factor=1", + "--override fetch.purgatory.purge.interval.requests=1000", + "--override group.max.session.timeout.ms=300000", + "--override group.min.session.timeout.ms=6000", + "--override log.cleaner.backoff.ms=15000", + "--override log.cleaner.dedupe.buffer.size=134217728", + "--override log.cleaner.delete.retention.ms=86400000", + "--override log.cleaner.enable=true", + "--override log.cleaner.io.buffer.load.factor=0.9", + "--override log.cleaner.io.buffer.size=524288", + "--override log.cleaner.io.max.bytes.per.second=1.7976931348623157E308", + "--override log.cleaner.min.cleanable.ratio=0.5", + "--override log.cleaner.min.compaction.lag.ms=0", + "--override log.cleaner.threads=1", + "--override log.cleanup.policy=delete", + "--override log.index.interval.bytes=4096", + "--override log.index.size.max.bytes=10485760", + "--override log.message.timestamp.difference.max.ms=9223372036854775807", + "--override log.message.timestamp.type=CreateTime", + "--override log.preallocate=false", + "--override log.retention.check.interval.ms=300000", + "--override max.connections.per.ip=2147483647", + f"--override num.partitions={config.num_partitions}", + "--override producer.purgatory.purge.interval.requests=1000", + "--override replica.fetch.backoff.ms=1000", + "--override replica.fetch.max.bytes=1048576", + "--override replica.fetch.response.max.bytes=10485760", + "--override reserved.broker.max.id=1000", + ] + ), + ] + ) + + container = container_builder.build() + + # Add container to pod spec + pod_spec_builder.add_container(container) + + return pod_spec_builder.build() + + +if __name__ == "__main__": + main(KafkaCharm) diff --git a/installers/charm/kafka/tests/__init__.py b/installers/charm/kafka/tests/__init__.py new file mode 100644 index 00000000..446d5cee --- /dev/null +++ b/installers/charm/kafka/tests/__init__.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright 2020 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +"""Init mocking for unit tests.""" + +import sys + + +import mock + + +class OCIImageResourceErrorMock(Exception): + pass + + +sys.path.append("src") + +oci_image = mock.MagicMock() +oci_image.OCIImageResourceError = OCIImageResourceErrorMock +sys.modules["oci_image"] = oci_image +sys.modules["oci_image"].OCIImageResource().fetch.return_value = {} diff --git a/installers/charm/kafka/tests/test_charm.py b/installers/charm/kafka/tests/test_charm.py new file mode 100644 index 00000000..ec0efbd2 --- /dev/null +++ b/installers/charm/kafka/tests/test_charm.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# Copyright 2020 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +from typing import NoReturn +import unittest +from unittest.mock import patch, PropertyMock + +from charm import KafkaCharm +from ops.model import ActiveStatus, BlockedStatus +from ops.testing import Harness + + +class TestCharm(unittest.TestCase): + """Kafka Charm unit tests.""" + + def setUp( + self, + ) -> NoReturn: + """Test setup""" + self.harness = Harness(KafkaCharm) + self.harness.set_leader(is_leader=True) + self.harness.begin() + self.config = {"num_partitions": 1} + self.harness.update_config(self.config) + + def test_config_changed_no_relations(self) -> NoReturn: + """Test config changed without relations.""" + self.harness.charm.on.config_changed.emit() + self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus) + + def test_config_changed_non_leader(self) -> NoReturn: + """Test config changed without relations (non-leader).""" + self.harness.set_leader(is_leader=False) + self.harness.charm.on.config_changed.emit() + + # Assertions + self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus) + + @patch("charm.KafkaCharm.num_units", new_callable=PropertyMock) + def test_with_relations_kafka( + self, mock_num_units + ) -> NoReturn: + "Test with relations (kafka)" + mock_num_units.return_value = 1 + + # Initializing the kafka relation + zookeeper_relation_id = self.harness.add_relation("zookeeper", "zookeeper") + self.harness.add_relation_unit(zookeeper_relation_id, "zookeeper/0") + self.harness.update_relation_data( + zookeeper_relation_id, "zookeeper", {"zookeeper_uri": "zk-uri"} + ) + + # Verifying status + self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus) + + +if __name__ == "__main__": + unittest.main() diff --git a/installers/charm/kafka/tox.ini b/installers/charm/kafka/tox.ini new file mode 100644 index 00000000..c341c8e4 --- /dev/null +++ b/installers/charm/kafka/tox.ini @@ -0,0 +1,126 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## +####################################################################################### + +[tox] +envlist = black, cover, flake8, pylint, yamllint, safety +skipsdist = true + +[tox:jenkins] +toxworkdir = /tmp/.tox + +[testenv] +basepython = python3.8 +setenv = VIRTUAL_ENV={envdir} + PYTHONDONTWRITEBYTECODE = 1 +deps = -r{toxinidir}/requirements.txt + + +####################################################################################### +[testenv:black] +deps = black +commands = + black --check --diff src/ tests/ + + +####################################################################################### +[testenv:cover] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + coverage + nose2 +commands = + sh -c 'rm -f nosetests.xml' + coverage erase + nose2 -C --coverage src + coverage report --omit='*tests*' + coverage html -d ./cover --omit='*tests*' + coverage xml -o coverage.xml --omit=*tests* +whitelist_externals = sh + + +####################################################################################### +[testenv:flake8] +deps = flake8 + flake8-import-order +commands = + flake8 src/ tests/ + + +####################################################################################### +[testenv:pylint] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + pylint +commands = + pylint -E src/ tests/ + + +####################################################################################### +[testenv:safety] +setenv = + LC_ALL=C.UTF-8 + LANG=C.UTF-8 +deps = {[testenv]deps} + safety +commands = + - safety check --full-report + + +####################################################################################### +[testenv:yamllint] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + yamllint +commands = yamllint . + +####################################################################################### +[testenv:build] +passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY +whitelist_externals = + charmcraft + sh +commands = + charmcraft build + sh -c 'ubuntu_version=20.04; \ + architectures="amd64-aarch64-arm64"; \ + charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \ + mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm' + +####################################################################################### +[flake8] +ignore = + W291, + W293, + W503, + E123, + E125, + E226, + E241, +exclude = + .git, + __pycache__, + .tox, +max-line-length = 120 +show-source = True +builtins = _ +max-complexity = 10 +import-order-style = google diff --git a/installers/charm/zookeeper/.gitignore b/installers/charm/zookeeper/.gitignore new file mode 100644 index 00000000..a85ce6d9 --- /dev/null +++ b/installers/charm/zookeeper/.gitignore @@ -0,0 +1,28 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +venv +.vscode +*.charm +.coverage +coverage.xml +.stestr +cover \ No newline at end of file diff --git a/installers/charm/zookeeper/.jujuignore b/installers/charm/zookeeper/.jujuignore new file mode 100644 index 00000000..3738c1c2 --- /dev/null +++ b/installers/charm/zookeeper/.jujuignore @@ -0,0 +1,32 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +venv +.vscode +*.charm +.coverage +coverage.xml +.gitignore +.stestr +cover +tests/ +requirements* +tox.ini diff --git a/installers/charm/zookeeper/.yamllint.yaml b/installers/charm/zookeeper/.yamllint.yaml new file mode 100644 index 00000000..5244c94f --- /dev/null +++ b/installers/charm/zookeeper/.yamllint.yaml @@ -0,0 +1,32 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +--- +extends: default + +yaml-files: + - "*.yaml" + - "*.yml" + - ".yamllint" +ignore: | + .tox + cover/ + venv diff --git a/installers/charm/zookeeper/README.md b/installers/charm/zookeeper/README.md new file mode 100644 index 00000000..bc6aec71 --- /dev/null +++ b/installers/charm/zookeeper/README.md @@ -0,0 +1,22 @@ + + +# Zookeeper operator Charm for Kubernetes + diff --git a/installers/charm/zookeeper/charmcraft.yaml b/installers/charm/zookeeper/charmcraft.yaml new file mode 100644 index 00000000..0a285a9d --- /dev/null +++ b/installers/charm/zookeeper/charmcraft.yaml @@ -0,0 +1,37 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +type: charm +bases: + - build-on: + - name: ubuntu + channel: "20.04" + architectures: ["amd64"] + run-on: + - name: ubuntu + channel: "20.04" + architectures: + - amd64 + - aarch64 + - arm64 +parts: + charm: + build-packages: [git] diff --git a/installers/charm/zookeeper/config.yaml b/installers/charm/zookeeper/config.yaml new file mode 100644 index 00000000..d9b89a41 --- /dev/null +++ b/installers/charm/zookeeper/config.yaml @@ -0,0 +1,89 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +options: + log_level: + description: | + Log level + type: string + default: INFO + image_pull_policy: + description: | + ImagePullPolicy configuration for the pod. + Possible values: always, ifnotpresent, never + type: string + default: always + min_session_timeout: + description: Min session timeout + type: int + default: 4000 + max_session_timeout: + description: Max session timeout + type: int + default: 40000 + purge_interval: + description: | + The time interval in hours for which the purge task has to be triggered. + Set to a positive integer (1 and above) to enable the auto purging. + type: int + default: 12 + snap_retain_count: + description: | + When enabled, ZooKeeper auto purge feature retains the + autopurge.snapRetainCount most recent snapshots and + the corresponding transaction logs in the dataDir and + dataLogDir respectively and deletes the rest. + Defaults to 3. Minimum value is 3. + type: int + default: 3 + max_client_cnxns: + description: | + Limits the number of concurrent connections (at the socket level) + that a single client, identified by IP address, may make to a single + member of the ZooKeeper ensemble. + type: int + default: 60 + heap: + description: Heap memory in Mega-bytes + type: int + default: 512 + sync_limit: + description: | + Amount of time, in ticks (see tickTime), to allow followers to sync + with ZooKeeper. + If followers fall too far behind a leader, they will be dropped. + type: int + default: 5 + init_limit: + description: | + Amount of time, in ticks (see tickTime), to allow followers to connect + and sync to a leader. Increased this value as needed, + if the amount of data managed by ZooKeeper is large. + type: int + default: 5 + tick_time: + description: | + The length of a single tick, which is the basic time unit used + by ZooKeeper, as measured in milliseconds. It is used to regulate + heartbeats, and timeouts. + For example, the minimum session timeout will be two ticks. + type: int + default: 2000 diff --git a/installers/charm/zookeeper/metadata.yaml b/installers/charm/zookeeper/metadata.yaml new file mode 100644 index 00000000..5a8bfb41 --- /dev/null +++ b/installers/charm/zookeeper/metadata.yaml @@ -0,0 +1,51 @@ +# Copyright 2020 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +name: zookeeper +summary: "Zookeeper charm for Kubernetes." +description: | + A CAAS charm to deploy zookeeper. +series: + - kubernetes +tags: + - kubernetes + - osm + - zookeeper +min-juju-version: 2.8.0 +resources: + image: + type: oci-image + description: OSM docker image for zookeeper + upstream-source: | + "rocks.canonical.com:443/k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10" +provides: + zookeeper: + interface: zookeeper +peers: + cluster: + interface: zookeeper-cluster +storage: + database: + type: filesystem + location: /var/lib/zookeeper +deployment: + type: stateful + service: cluster diff --git a/installers/charm/zookeeper/requirements-test.txt b/installers/charm/zookeeper/requirements-test.txt new file mode 100644 index 00000000..316f6d20 --- /dev/null +++ b/installers/charm/zookeeper/requirements-test.txt @@ -0,0 +1,21 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net + +mock==4.0.3 diff --git a/installers/charm/zookeeper/requirements.txt b/installers/charm/zookeeper/requirements.txt new file mode 100644 index 00000000..1a8928c7 --- /dev/null +++ b/installers/charm/zookeeper/requirements.txt @@ -0,0 +1,22 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +git+https://github.com/charmed-osm/ops-lib-charmed-osm/@master \ No newline at end of file diff --git a/installers/charm/zookeeper/src/charm.py b/installers/charm/zookeeper/src/charm.py new file mode 100755 index 00000000..6e4588c4 --- /dev/null +++ b/installers/charm/zookeeper/src/charm.py @@ -0,0 +1,181 @@ +#!/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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +# pylint: disable=E0213 + +import logging +from typing import NoReturn + + +from ops.framework import EventBase +from ops.main import main +from opslib.osm.charm import CharmedOsmBase +from opslib.osm.interfaces.zookeeper import ZookeeperCluster, ZookeeperServer +from opslib.osm.pod import ContainerV3Builder, PodSpecV3Builder +from opslib.osm.validator import ModelValidator, validator + +logger = logging.getLogger(__name__) + +CLIENT_PORT = 2181 +SERVER_PORT = 2888 +LEADER_ELECTION_PORT = 3888 + + +class ConfigModel(ModelValidator): + log_level: str + image_pull_policy: str + min_session_timeout: int + max_session_timeout: int + purge_interval: int + snap_retain_count: int + max_client_cnxns: int + heap: int + sync_limit: int + init_limit: int + tick_time: int + + @validator("log_level") + def validate_log_level(cls, v): + if v not in {"INFO", "DEBUG"}: + raise ValueError("value must be INFO or DEBUG") + return v + + @validator("image_pull_policy") + def validate_image_pull_policy(cls, v): + values = { + "always": "Always", + "ifnotpresent": "IfNotPresent", + "never": "Never", + } + v = v.lower() + if v not in values.keys(): + raise ValueError("value must be always, ifnotpresent or never") + return values[v] + + +class ZookeeperCharm(CharmedOsmBase): + """Zookeeper Charm.""" + + def __init__(self, *args) -> NoReturn: + """Zookeeper Charm constructor.""" + super().__init__(*args, oci_image="image") + # Initialize Zookeeper cluster relation + self.zookeeper_cluster = ZookeeperCluster(self, "cluster", CLIENT_PORT) + self.framework.observe(self.on["cluster"].relation_changed, self._setup_cluster) + # Initialize Zookeeper relation + self.zookeeper_server = ZookeeperServer(self, "zookeeper") + self.framework.observe(self.on["zookeeper"].relation_joined, self._publish_info) + + @property + def num_units(self): + return self.zookeeper_cluster.num_units + + @property + def zookeeper_uri(self): + return self.zookeeper_cluster.zookeeper_uri + + def _setup_cluster(self, event: EventBase): + """Publishes Zookeeper information and reconfigures the pod. + + Args: + event (EventBase): Zookeeper Cluster relation event. + """ + self._publish_zookeeper_info(event) + self.configure_pod() + + def _publish_info(self, event: EventBase): + """Publishes Zookeeper information. + + Args: + event (EventBase): Zookeeper relation event. + """ + if self.unit.is_leader(): + zk_uri = self.zookeeper_uri + if zk_uri: + self.zookeeper_server.publish_info(zk_uri) + else: + event.defer() + + def build_pod_spec(self, image_info): + # Validate config + config = ConfigModel(**dict(self.config)) + + # Create Builder for the PodSpec + pod_spec_builder = PodSpecV3Builder() + + # Build Container + container_builder = ContainerV3Builder( + self.app.name, image_info, config.image_pull_policy + ) + + container_builder.add_port(name="client", port=CLIENT_PORT) + container_builder.add_port(name="server", port=SERVER_PORT) + container_builder.add_port(name="leader-election", port=LEADER_ELECTION_PORT) + container_builder.add_tcpsocket_readiness_probe( + CLIENT_PORT, + initial_delay_seconds=10, + timeout_seconds=5, + failure_threshold=6, + success_threshold=1, + ) + container_builder.add_tcpsocket_liveness_probe( + CLIENT_PORT, initial_delay_seconds=20 + ) + container_builder.add_command( + [ + "sh", + "-c", + " ".join( + [ + "start-zookeeper", + f"--servers={self.num_units}", + "--data_dir=/var/lib/zookeeper/data", + "--data_log_dir=/var/lib/zookeeper/data/log", + "--conf_dir=/opt/zookeeper/conf", + f"--client_port={CLIENT_PORT}", + f"--election_port={LEADER_ELECTION_PORT}", + f"--server_port={SERVER_PORT}", + f"--tick_time={config.tick_time}", + f"--init_limit={config.init_limit}", + f"--sync_limit={config.sync_limit}", + f"--heap={config.heap}M", + f"--max_client_cnxns={config.max_client_cnxns}", + f"--snap_retain_count={config.snap_retain_count}", + f"--purge_interval={config.purge_interval}", + f"--max_session_timeout={config.max_session_timeout}", + f"--min_session_timeout={config.min_session_timeout}", + f"--log_level={config.log_level}", + ] + ), + ] + ) + + container = container_builder.build() + + # Add container to pod spec + pod_spec_builder.add_container(container) + + return pod_spec_builder.build() + + +if __name__ == "__main__": + main(ZookeeperCharm) diff --git a/installers/charm/zookeeper/tests/__init__.py b/installers/charm/zookeeper/tests/__init__.py new file mode 100644 index 00000000..446d5cee --- /dev/null +++ b/installers/charm/zookeeper/tests/__init__.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright 2020 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +"""Init mocking for unit tests.""" + +import sys + + +import mock + + +class OCIImageResourceErrorMock(Exception): + pass + + +sys.path.append("src") + +oci_image = mock.MagicMock() +oci_image.OCIImageResourceError = OCIImageResourceErrorMock +sys.modules["oci_image"] = oci_image +sys.modules["oci_image"].OCIImageResource().fetch.return_value = {} diff --git a/installers/charm/zookeeper/tests/test_charm.py b/installers/charm/zookeeper/tests/test_charm.py new file mode 100644 index 00000000..27d3401c --- /dev/null +++ b/installers/charm/zookeeper/tests/test_charm.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Copyright 2020 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## + +from typing import NoReturn +import unittest +from unittest.mock import patch, PropertyMock + +from charm import ZookeeperCharm +from ops.model import ActiveStatus, BlockedStatus +from ops.testing import Harness + + +class TestCharm(unittest.TestCase): + """Zookeeper Charm unit tests.""" + + def setUp( + self, + ) -> NoReturn: + """Test setup""" + self.harness = Harness(ZookeeperCharm) + self.harness.set_leader(is_leader=True) + self.config = {"log_level": "INFO", "image_pull_pulicy": "always"} + self.harness.begin() + + def test_config_invalid_log_level(self) -> NoReturn: + """Test invalid log_level config.""" + self.config.update({"log_level": "invalid log level"}) + self.harness.update_config(self.config) + self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus) + + def test_config_invalid_image_pull_pulicy(self) -> NoReturn: + """Test invalid image_pull_pulicy config.""" + self.config.update({"image_pull_policy": "invalid image_pull_policy"}) + self.harness.update_config(self.config) + self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus) + + @patch("charm.ZookeeperCharm.num_units", new_callable=PropertyMock) + def test_config_changed_no_relations(self, mock_num_units) -> NoReturn: + """Test config changed without relations.""" + mock_num_units.return_value = 1 + self.harness.charm.on.config_changed.emit() + self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus) + + @patch("charm.ZookeeperCharm.num_units", new_callable=PropertyMock) + def test_config_changed_non_leader(self, mock_num_units) -> NoReturn: + """Test config changed without relations (non-leader).""" + mock_num_units.return_value = 1 + self.harness.set_leader(is_leader=False) + self.harness.charm.on.config_changed.emit() + + # Assertions + self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus) + + @patch("charm.ZookeeperCharm.num_units", new_callable=PropertyMock) + @patch("charm.ZookeeperCharm.zookeeper_uri", new_callable=PropertyMock) + def test_with_relations_zookeeper( + self, mock_zookeeper_uri, mock_num_units + ) -> NoReturn: + "Test with relations (zookeeper)" + mock_num_units.return_value = 1 + mock_zookeeper_uri.return_value = "zk-uri" + + # Initializing the zookeeper relation + zookeeper_relation_id = self.harness.add_relation("zookeeper", "kafka") + self.harness.add_relation_unit(zookeeper_relation_id, "kafka/0") + # self.harness.update_relation_data( + # zookeeper_relation_id, "kafka/0", {"host": "zookeeper", "port": 9092} + # ) + + # Verifying status + self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus) + + +if __name__ == "__main__": + unittest.main() diff --git a/installers/charm/zookeeper/tox.ini b/installers/charm/zookeeper/tox.ini new file mode 100644 index 00000000..c341c8e4 --- /dev/null +++ b/installers/charm/zookeeper/tox.ini @@ -0,0 +1,126 @@ +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: legal@canonical.com +# +# To get in touch with the maintainers, please contact: +# osm-charmers@lists.launchpad.net +## +####################################################################################### + +[tox] +envlist = black, cover, flake8, pylint, yamllint, safety +skipsdist = true + +[tox:jenkins] +toxworkdir = /tmp/.tox + +[testenv] +basepython = python3.8 +setenv = VIRTUAL_ENV={envdir} + PYTHONDONTWRITEBYTECODE = 1 +deps = -r{toxinidir}/requirements.txt + + +####################################################################################### +[testenv:black] +deps = black +commands = + black --check --diff src/ tests/ + + +####################################################################################### +[testenv:cover] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + coverage + nose2 +commands = + sh -c 'rm -f nosetests.xml' + coverage erase + nose2 -C --coverage src + coverage report --omit='*tests*' + coverage html -d ./cover --omit='*tests*' + coverage xml -o coverage.xml --omit=*tests* +whitelist_externals = sh + + +####################################################################################### +[testenv:flake8] +deps = flake8 + flake8-import-order +commands = + flake8 src/ tests/ + + +####################################################################################### +[testenv:pylint] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + pylint +commands = + pylint -E src/ tests/ + + +####################################################################################### +[testenv:safety] +setenv = + LC_ALL=C.UTF-8 + LANG=C.UTF-8 +deps = {[testenv]deps} + safety +commands = + - safety check --full-report + + +####################################################################################### +[testenv:yamllint] +deps = {[testenv]deps} + -r{toxinidir}/requirements-test.txt + yamllint +commands = yamllint . + +####################################################################################### +[testenv:build] +passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY +whitelist_externals = + charmcraft + sh +commands = + charmcraft build + sh -c 'ubuntu_version=20.04; \ + architectures="amd64-aarch64-arm64"; \ + charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \ + mv $charm_name"_ubuntu-"$ubuntu_version-$architectures.charm $charm_name.charm' + +####################################################################################### +[flake8] +ignore = + W291, + W293, + W503, + E123, + E125, + E226, + E241, +exclude = + .git, + __pycache__, + .tox, +max-line-length = 120 +show-source = True +builtins = _ +max-complexity = 10 +import-order-style = google -- 2.25.1