--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+<!-- 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
+
--- /dev/null
+# 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]
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+#!/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)
--- /dev/null
+#!/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 = {}
--- /dev/null
+#!/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()
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+<!-- 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
+
--- /dev/null
+# 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]
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+#!/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)
--- /dev/null
+#!/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 = {}
--- /dev/null
+#!/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()
--- /dev/null
+# 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