diff --git a/osci.yaml b/osci.yaml index ffb11ce..3d945ad 100644 --- a/osci.yaml +++ b/osci.yaml @@ -7,4 +7,4 @@ build_type: charmcraft publish_charm: true charmcraft_channel: 2.0/stable - publish_channel: latest/edge + publish_channel: 21.09/edge diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2896bc0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,39 @@ +# Copyright 2022 Canonical Ltd. +# See LICENSE file for licensing details. + +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Formatting tools configuration +[tool.black] +line-length = 79 + +[tool.isort] +profile = "black" +multi_line_output = 3 +force_grid_wrap = true + +# Linting tools configuration +[tool.flake8] +max-line-length = 79 +max-doc-length = 99 +max-complexity = 10 +exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] +select = ["E", "W", "F", "C", "N", "R", "D", "H"] +# Ignore W503, E501 because using black creates errors with this +# Ignore D107 Missing docstring in __init__ +ignore = ["W503", "E501", "D107", "E402"] +per-file-ignores = [] +docstring-convention = "google" +# Check for properly formatted copyright header in each file +copyright-check = "True" +copyright-author = "Canonical Ltd." +copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/src/charm.py b/src/charm.py index de7eae0..902142e 100755 --- a/src/charm.py +++ b/src/charm.py @@ -25,21 +25,30 @@ develop a new k8s charm using the Operator Framework: """ import logging - -from ipaddress import IPv4Address, IPv6Address -from typing import List, Mapping, Union - -from ops.framework import StoredState -from ops.main import main +from ipaddress import ( + IPv4Address, + IPv6Address, +) +from typing import ( + List, + Mapping, + Union, +) import ops_sunbeam.config_contexts as sunbeam_ctxts import ops_sunbeam.core as sunbeam_core -import ops_sunbeam.ovn.container_handlers as ovn_chandlers -import ops_sunbeam.ovn.config_contexts as ovn_ctxts import ops_sunbeam.ovn.charm as ovn_charm - -from charms.observability_libs.v0.kubernetes_service_patch \ - import KubernetesServicePatch +import ops_sunbeam.ovn.config_contexts as ovn_ctxts +import ops_sunbeam.ovn.container_handlers as ovn_chandlers +from charms.observability_libs.v0.kubernetes_service_patch import ( + KubernetesServicePatch, +) +from ops.framework import ( + StoredState, +) +from ops.main import ( + main, +) logger = logging.getLogger(__name__) @@ -47,18 +56,22 @@ OVSDB_SERVER = "ovsdb-server" class OVNRelayPebbleHandler(ovn_chandlers.OVNPebbleHandler): + """Handler for OVN Relay container.""" @property def wrapper_script(self): - return '/root/ovn-relay-wrapper.sh' + """Wrapper script for managing OVN service.""" + return "/root/ovn-relay-wrapper.sh" @property def status_command(self): - return '/usr/share/ovn/scripts/ovn-ctl status_ovsdb' + """Status command for container.""" + return "/usr/share/ovn/scripts/ovn-ctl status_ovsdb" @property def service_description(self): - return 'OVN Relay' + """Description of service.""" + return "OVN Relay" def init_service(self, context: sunbeam_core.OPSCharmContexts) -> None: """Initialise service ready for use. @@ -79,8 +92,8 @@ class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm): _state = StoredState() mandatory_relations = { - 'ovsdb-cms', - 'certificates', + "ovsdb-cms", + "certificates", } def __init__(self, framework): @@ -88,13 +101,13 @@ class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm): self.service_patcher = KubernetesServicePatch( self, [ - ('southbound', 6642), + ("southbound", 6642), ], - service_type="LoadBalancer" + service_type="LoadBalancer", ) self.framework.observe( self.on.get_southbound_db_url_action, - self._get_southbound_db_url_action + self._get_southbound_db_url_action, ) def _get_southbound_db_url_action(self, event): @@ -104,7 +117,8 @@ class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm): def ingress_address(self) -> Union[IPv4Address, IPv6Address]: """Network IP address for access to the OVN relay service.""" return self.model.get_binding( - 'ovsdb-cms-relay').network.ingress_addresses[0] + "ovsdb-cms-relay" + ).network.ingress_addresses[0] @property def southbound_db_url(self) -> str: @@ -112,23 +126,24 @@ class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm): return f"ssl:{self.ingress_address}:6642" def get_pebble_handlers(self): + """Return pebble handler for container.""" pebble_handlers = [ OVNRelayPebbleHandler( self, OVSDB_SERVER, - 'ovn-relay', + "ovn-relay", self.container_configs, self.template_dir, - self.openstack_release, - self.configure_charm)] + self.configure_charm, + ) + ] return pebble_handlers @property def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]: """Configuration contexts for the operator.""" contexts = super().config_contexts - contexts.append( - ovn_ctxts.OVNDBConfigContext(self, "ovs_db")) + contexts.append(ovn_ctxts.OVNDBConfigContext(self, "ovs_db")) return contexts @property @@ -141,12 +156,7 @@ class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm): return {} -class OVNRelayXenaOperatorCharm(OVNRelayOperatorCharm): - - openstack_release = 'xena' - - if __name__ == "__main__": # Note: use_juju_for_storage=True required per # https://github.com/canonical/operator/issues/506 - main(OVNRelayXenaOperatorCharm, use_juju_for_storage=True) + main(OVNRelayOperatorCharm, use_juju_for_storage=True) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index e69de29..304f420 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for charm.""" diff --git a/tests/unit/test_ovn_relay_charm.py b/tests/unit/test_ovn_relay_charm.py index b81e4ce..f4654d6 100644 --- a/tests/unit/test_ovn_relay_charm.py +++ b/tests/unit/test_ovn_relay_charm.py @@ -14,12 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import charm +"""Tests for OVN relay.""" + import ops_sunbeam.test_utils as test_utils +import charm -class _OVNRelayXenaOperatorCharm(charm.OVNRelayXenaOperatorCharm): +class _OVNRelayOperatorCharm(charm.OVNRelayOperatorCharm): def __init__(self, framework): self.seen_events = [] super().__init__(framework) @@ -32,40 +34,45 @@ class _OVNRelayXenaOperatorCharm(charm.OVNRelayXenaOperatorCharm): self._log_event(event) -class TestOVNRelayXenaOperatorCharm(test_utils.CharmTestCase): +class TestOVNRelayOperatorCharm(test_utils.CharmTestCase): + """Test OVN relay.""" PATCHES = [ - 'KubernetesServicePatch', + "KubernetesServicePatch", ] def setUp(self): + """Setup OVN relay tests.""" super().setUp(charm, self.PATCHES) self.harness = test_utils.get_harness( - _OVNRelayXenaOperatorCharm, - container_calls=self.container_calls) + _OVNRelayOperatorCharm, container_calls=self.container_calls + ) self.addCleanup(self.harness.cleanup) self.harness.begin() def test_pebble_ready_handler(self): + """Test Pebble ready event is captured.""" self.assertEqual(self.harness.charm.seen_events, []) - self.harness.container_pebble_ready('ovsdb-server') + self.harness.container_pebble_ready("ovsdb-server") self.assertEqual(len(self.harness.charm.seen_events), 1) def test_all_relations(self): + """Test all the charms relations.""" self.harness.set_leader() self.assertEqual(self.harness.charm.seen_events, []) test_utils.set_all_pebbles_ready(self.harness) test_utils.add_all_relations(self.harness) ovsdb_config_files = [ - '/etc/ovn/cert_host', - '/etc/ovn/key_host', - '/etc/ovn/ovn-central.crt', - '/root/ovn-relay-wrapper.sh'] + "/etc/ovn/cert_host", + "/etc/ovn/key_host", + "/etc/ovn/ovn-central.crt", + "/root/ovn-relay-wrapper.sh", + ] for f in ovsdb_config_files: - self.check_file('ovsdb-server', f) + self.check_file("ovsdb-server", f) def test_southbound_db_url(self): + """Return southbound db url.""" self.assertEqual( - 'ssl:10.0.0.10:6642', - self.harness.charm.southbound_db_url + "ssl:10.0.0.10:6642", self.harness.charm.southbound_db_url ) diff --git a/tox.ini b/tox.ini index cfa25bf..ea682f9 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,8 @@ minversion = 3.18.0 src_path = {toxinidir}/src/ tst_path = {toxinidir}/tests/ lib_path = {toxinidir}/lib/ +pyproject_toml = {toxinidir}/pyproject.toml +all_path = {[vars]src_path} {[vars]tst_path} [testenv] basepython = python3 @@ -33,6 +35,15 @@ allowlist_externals = deps = -r{toxinidir}/test-requirements.txt +[testenv:fmt] +description = Apply coding style standards to code +deps = + black + isort +commands = + isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox + black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} + [testenv:build] basepython = python3 deps = @@ -64,11 +75,6 @@ deps = {[testenv:py3]deps} basepython = python3.10 deps = {[testenv:py3]deps} -[testenv:pep8] -basepython = python3 -deps = {[testenv]deps} -commands = flake8 {posargs} {[vars]src_path} {[vars]tst_path} - [testenv:cover] basepython = python3 deps = {[testenv:py3]deps} @@ -83,6 +89,31 @@ commands = coverage xml -o cover/coverage.xml coverage report +[testenv:pep8] +description = Alias for lint +deps = {[testenv:lint]deps} +commands = {[testenv:lint]commands} + +[testenv:lint] +description = Check code against coding style standards +deps = + black + # flake8==4.0.1 # Pin version until https://github.com/csachs/pyproject-flake8/pull/14 is merged + flake8 + flake8-docstrings + flake8-copyright + flake8-builtins + pyproject-flake8 + pep8-naming + isort + codespell +commands = + codespell {[vars]all_path} + # pflake8 wrapper supports config from pyproject.toml + pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} + isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} + black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} + [testenv:func-noop] basepython = python3 commands =