Add Kafka and Zookeeper charms in operator framework 28/11128/5
authorDavid Garcia <david.garcia@canonical.com>
Mon, 30 Aug 2021 16:01:22 +0000 (18:01 +0200)
committersousaedu <eduardo.sousa@canonical.com>
Tue, 31 Aug 2021 12:43:45 +0000 (14:43 +0200)
Change-Id: I15645825ab8ff927ad0f72bbfd53ea71343b2be4
Signed-off-by: David Garcia <david.garcia@canonical.com>
26 files changed:
installers/charm/kafka/.gitignore [new file with mode: 0644]
installers/charm/kafka/.jujuignore [new file with mode: 0644]
installers/charm/kafka/.yamllint.yaml [new file with mode: 0644]
installers/charm/kafka/README.md [new file with mode: 0644]
installers/charm/kafka/charmcraft.yaml [new file with mode: 0644]
installers/charm/kafka/config.yaml [new file with mode: 0644]
installers/charm/kafka/metadata.yaml [new file with mode: 0644]
installers/charm/kafka/requirements-test.txt [new file with mode: 0644]
installers/charm/kafka/requirements.txt [new file with mode: 0644]
installers/charm/kafka/src/charm.py [new file with mode: 0755]
installers/charm/kafka/tests/__init__.py [new file with mode: 0644]
installers/charm/kafka/tests/test_charm.py [new file with mode: 0644]
installers/charm/kafka/tox.ini [new file with mode: 0644]
installers/charm/zookeeper/.gitignore [new file with mode: 0644]
installers/charm/zookeeper/.jujuignore [new file with mode: 0644]
installers/charm/zookeeper/.yamllint.yaml [new file with mode: 0644]
installers/charm/zookeeper/README.md [new file with mode: 0644]
installers/charm/zookeeper/charmcraft.yaml [new file with mode: 0644]
installers/charm/zookeeper/config.yaml [new file with mode: 0644]
installers/charm/zookeeper/metadata.yaml [new file with mode: 0644]
installers/charm/zookeeper/requirements-test.txt [new file with mode: 0644]
installers/charm/zookeeper/requirements.txt [new file with mode: 0644]
installers/charm/zookeeper/src/charm.py [new file with mode: 0755]
installers/charm/zookeeper/tests/__init__.py [new file with mode: 0644]
installers/charm/zookeeper/tests/test_charm.py [new file with mode: 0644]
installers/charm/zookeeper/tox.ini [new file with mode: 0644]

diff --git a/installers/charm/kafka/.gitignore b/installers/charm/kafka/.gitignore
new file mode 100644 (file)
index 0000000..a85ce6d
--- /dev/null
@@ -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 (file)
index 0000000..3738c1c
--- /dev/null
@@ -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 (file)
index 0000000..5244c94
--- /dev/null
@@ -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 (file)
index 0000000..851adae
--- /dev/null
@@ -0,0 +1,22 @@
+<!-- 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 -->
+
+# Kafka operator Charm for Kubernetes
+
diff --git a/installers/charm/kafka/charmcraft.yaml b/installers/charm/kafka/charmcraft.yaml
new file mode 100644 (file)
index 0000000..0a285a9
--- /dev/null
@@ -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 (file)
index 0000000..4319a57
--- /dev/null
@@ -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 (file)
index 0000000..d154406
--- /dev/null
@@ -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 (file)
index 0000000..316f6d2
--- /dev/null
@@ -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 (file)
index 0000000..1a8928c
--- /dev/null
@@ -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 (executable)
index 0000000..763d416
--- /dev/null
@@ -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 (file)
index 0000000..446d5ce
--- /dev/null
@@ -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 (file)
index 0000000..ec0efbd
--- /dev/null
@@ -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 (file)
index 0000000..c341c8e
--- /dev/null
@@ -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 (file)
index 0000000..a85ce6d
--- /dev/null
@@ -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 (file)
index 0000000..3738c1c
--- /dev/null
@@ -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 (file)
index 0000000..5244c94
--- /dev/null
@@ -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 (file)
index 0000000..bc6aec7
--- /dev/null
@@ -0,0 +1,22 @@
+<!-- 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 -->
+
+# Zookeeper operator Charm for Kubernetes
+
diff --git a/installers/charm/zookeeper/charmcraft.yaml b/installers/charm/zookeeper/charmcraft.yaml
new file mode 100644 (file)
index 0000000..0a285a9
--- /dev/null
@@ -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 (file)
index 0000000..d9b89a4
--- /dev/null
@@ -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 (file)
index 0000000..5a8bfb4
--- /dev/null
@@ -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 (file)
index 0000000..316f6d2
--- /dev/null
@@ -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 (file)
index 0000000..1a8928c
--- /dev/null
@@ -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 (executable)
index 0000000..6e4588c
--- /dev/null
@@ -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 (file)
index 0000000..446d5ce
--- /dev/null
@@ -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 (file)
index 0000000..27d3401
--- /dev/null
@@ -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 (file)
index 0000000..c341c8e
--- /dev/null
@@ -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