CharmHub and new kafka and zookeeper charms 95/11695/6
authorDavid Garcia <david.garcia@canonical.com>
Mon, 21 Feb 2022 10:48:11 +0000 (11:48 +0100)
committerbeierlm <mark.beierl@canonical.com>
Tue, 22 Feb 2022 17:57:16 +0000 (18:57 +0100)
- Charmed installer uses bundles published in CharmHub
- Use new zookeeper and kafka sidecar-charm
- Changes in the charms to integrate with the new Kafka

Change-Id: Ie59fe1c7c72774b317abe2433fafb28a11472b72
Signed-off-by: David Garcia <david.garcia@canonical.com>
40 files changed:
installers/charm/build.sh
installers/charm/bundles/.gitignore [new file with mode: 0644]
installers/charm/bundles/osm-ha/bundle.yaml
installers/charm/bundles/osm/bundle.yaml
installers/charm/grafana/tox.ini
installers/charm/kafka-exporter/lib/charms/kafka_k8s/v0/kafka.py [new file with mode: 0644]
installers/charm/kafka-exporter/src/charm.py
installers/charm/kafka-exporter/tests/test_charm.py
installers/charm/kafka-exporter/tox.ini
installers/charm/keystone/tox.ini
installers/charm/lcm/lib/charms/kafka_k8s/v0/kafka.py [new file with mode: 0644]
installers/charm/lcm/src/charm.py
installers/charm/lcm/tests/test_charm.py
installers/charm/lcm/tox.ini
installers/charm/local_osm_bundle.yaml
installers/charm/local_osm_ha_bundle.yaml
installers/charm/mon/lib/charms/kafka_k8s/v0/kafka.py [new file with mode: 0644]
installers/charm/mon/src/charm.py
installers/charm/mon/tests/test_charm.py
installers/charm/mon/tox.ini
installers/charm/mongodb-exporter/tox.ini
installers/charm/nbi/lib/charms/kafka_k8s/v0/kafka.py [new file with mode: 0644]
installers/charm/nbi/src/charm.py
installers/charm/nbi/tests/test_charm.py
installers/charm/nbi/tox.ini
installers/charm/ng-ui/tox.ini
installers/charm/pla/lib/charms/kafka_k8s/v0/kafka.py [new file with mode: 0644]
installers/charm/pla/src/charm.py
installers/charm/pla/tests/test_charm.py
installers/charm/pla/tox.ini
installers/charm/pol/lib/charms/kafka_k8s/v0/kafka.py [new file with mode: 0644]
installers/charm/pol/src/charm.py
installers/charm/pol/tests/test_charm.py
installers/charm/pol/tox.ini
installers/charm/prometheus/tox.ini
installers/charm/ro/lib/charms/kafka_k8s/v0/kafka.py [new file with mode: 0644]
installers/charm/ro/src/charm.py
installers/charm/ro/tests/test_charm.py
installers/charm/ro/tox.ini
installers/charmed_install.sh

index 65dd87d..459da13 100755 (executable)
@@ -17,12 +17,12 @@ function build() {
     cd $1 && tox -qe build && cd ..
 }
 
-charms="ro nbi pla pol mon lcm ng-ui keystone grafana prometheus mariadb-k8s mongodb-k8s zookeeper-k8s kafka-k8s mongodb-exporter kafka-exporter mysqld-exporter"
+charms="ro nbi pla pol mon lcm ng-ui grafana prometheus mongodb-exporter kafka-exporter mysqld-exporter"
 if [ -z `which charmcraft` ]; then
-    sudo snap install charmcraft --edge
+    sudo snap install charmcraft --classic
 fi
 
 for charm_directory in $charms; do
-    build $charm_directory &
+    build $charm_directory
 done
 wait
\ No newline at end of file
diff --git a/installers/charm/bundles/.gitignore b/installers/charm/bundles/.gitignore
new file mode 100644 (file)
index 0000000..00b9f63
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright 2022 ETSI
+#
+# 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.
+
+*.zip
+*build/
\ No newline at end of file
index 202ee2b..f20e598 100644 (file)
@@ -30,17 +30,24 @@ description: |
   - Availability of managed services
 applications:
   zookeeper:
-    charm: osm-zookeeper
+    charm: zookeeper-k8s
     channel: latest/edge
     scale: 3
-    series: kubernetes
     storage:
-      database: 100M
-    options:
-      zookeeper-units: 3
+      data: 100M
     annotations:
       gui-x: 0
       gui-y: 500
+  kafka:
+    charm: kafka-k8s
+    channel: latest/edge
+    scale: 3
+    trust: true
+    storage:
+      data: 100M
+    annotations:
+      gui-x: 0
+      gui-y: 250
   mariadb:
     charm: charmed-osm-mariadb-k8s
     scale: 3
@@ -55,19 +62,6 @@ applications:
     annotations:
       gui-x: -300
       gui-y: -250
-  kafka:
-    charm: osm-kafka
-    channel: latest/edge
-    scale: 3
-    series: kubernetes
-    storage:
-      database: 100M
-    options:
-      zookeeper-units: 3
-      kafka-units: 3
-    annotations:
-      gui-x: 0
-      gui-y: 250
   mongodb:
     charm: mongodb-k8s
     channel: latest/stable
@@ -87,6 +81,8 @@ applications:
       database_commonkey: osm
       auth_backend: keystone
       log_level: DEBUG
+    resources:
+      image: opensourcemano/nbi:testing-daily
     annotations:
       gui-x: 0
       gui-y: -250
@@ -97,6 +93,8 @@ applications:
     series: kubernetes
     options:
       log_level: DEBUG
+    resources:
+      image: opensourcemano/ro:testing-daily
     annotations:
       gui-x: -300
       gui-y: 250
@@ -105,6 +103,8 @@ applications:
     channel: latest/edge
     scale: 3
     series: kubernetes
+    resources:
+      image: opensourcemano/ng-ui:testing-daily
     annotations:
       gui-x: 600
       gui-y: 0
@@ -116,6 +116,8 @@ applications:
     options:
       database_commonkey: osm
       log_level: DEBUG
+    resources:
+      image: opensourcemano/lcm:testing-daily
     annotations:
       gui-x: -300
       gui-y: 0
@@ -128,6 +130,8 @@ applications:
       database_commonkey: osm
       log_level: DEBUG
       keystone_enabled: true
+    resources:
+      image: opensourcemano/mon:testing-daily
     annotations:
       gui-x: 300
       gui-y: 0
@@ -138,6 +142,8 @@ applications:
     series: kubernetes
     options:
       log_level: DEBUG
+    resources:
+      image: opensourcemano/pol:testing-daily
     annotations:
       gui-x: -300
       gui-y: 500
@@ -148,6 +154,8 @@ applications:
     series: kubernetes
     options:
       log_level: DEBUG
+    resources:
+      image: opensourcemano/pla:testing-daily
     annotations:
       gui-x: 600
       gui-y: -250
@@ -175,6 +183,8 @@ applications:
     charm: osm-keystone
     channel: latest/edge
     scale: 1
+    resources:
+      keystone-image: opensourcemano/keystone:testing-daily
     annotations:
       gui-x: 300
       gui-y: -250
index fa367dc..601e365 100644 (file)
@@ -29,15 +29,24 @@ description: |
   - Availability of managed services
 applications:
   zookeeper:
-    charm: osm-zookeeper
+    charm: zookeeper-k8s
     channel: latest/edge
     scale: 1
-    series: kubernetes
     storage:
-      database: 100M
+      data: 100M
     annotations:
       gui-x: 0
       gui-y: 500
+  kafka:
+    charm: kafka-k8s
+    channel: latest/edge
+    scale: 1
+    trust: true
+    storage:
+      data: 100M
+    annotations:
+      gui-x: 0
+      gui-y: 250
   mariadb:
     charm: charmed-osm-mariadb-k8s
     scale: 1
@@ -51,16 +60,6 @@ applications:
     annotations:
       gui-x: -300
       gui-y: -250
-  kafka:
-    charm: osm-kafka
-    channel: latest/edge
-    scale: 1
-    series: kubernetes
-    storage:
-      database: 100M
-    annotations:
-      gui-x: 0
-      gui-y: 250
   mongodb:
     charm: mongodb-k8s
     channel: latest/stable
@@ -80,6 +79,8 @@ applications:
       database_commonkey: osm
       auth_backend: keystone
       log_level: DEBUG
+    resources:
+      image: opensourcemano/nbi:testing-daily
     annotations:
       gui-x: 0
       gui-y: -250
@@ -90,6 +91,8 @@ applications:
     series: kubernetes
     options:
       log_level: DEBUG
+    resources:
+      image: opensourcemano/ro:testing-daily
     annotations:
       gui-x: -300
       gui-y: 250
@@ -98,6 +101,8 @@ applications:
     channel: latest/edge
     scale: 1
     series: kubernetes
+    resources:
+      image: opensourcemano/ng-ui:testing-daily
     annotations:
       gui-x: 600
       gui-y: 0
@@ -109,6 +114,8 @@ applications:
     options:
       database_commonkey: osm
       log_level: DEBUG
+    resources:
+      image: opensourcemano/lcm:testing-daily
     annotations:
       gui-x: -300
       gui-y: 0
@@ -121,6 +128,8 @@ applications:
       database_commonkey: osm
       log_level: DEBUG
       keystone_enabled: true
+    resources:
+      image: opensourcemano/mon:testing-daily
     annotations:
       gui-x: 300
       gui-y: 0
@@ -131,6 +140,8 @@ applications:
     series: kubernetes
     options:
       log_level: DEBUG
+    resources:
+      image: opensourcemano/pol:testing-daily
     annotations:
       gui-x: -300
       gui-y: 500
@@ -141,6 +152,8 @@ applications:
     series: kubernetes
     options:
       log_level: DEBUG
+    resources:
+      image: opensourcemano/pla:testing-daily
     annotations:
       gui-x: 600
       gui-y: -250
@@ -168,6 +181,8 @@ applications:
     charm: osm-keystone
     channel: latest/edge
     scale: 1
+    resources:
+      keystone-image: opensourcemano/keystone:testing-daily
     annotations:
       gui-x: 300
       gui-y: -250
index ab5c86d..58e13a6 100644 (file)
@@ -99,7 +99,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
diff --git a/installers/charm/kafka-exporter/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/kafka-exporter/lib/charms/kafka_k8s/v0/kafka.py
new file mode 100644 (file)
index 0000000..1baf9a8
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""Kafka library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`kafka` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
+
+Any Charmed Operator that *requires* Kafka for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Kafka would look like
+
+```
+$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  kafka:
+    interface: kafka
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = KafkaEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(
+            self.on.kafka_available,
+            self._on_kafka_available,
+        )
+        self.framework.observe(
+            self.on.kafka_broken,
+            self._on_kafka_broken,
+        )
+
+    def _on_kafka_available(self, event):
+        # Get Kafka host and port
+        host: str = self.kafka.host
+        port: int = self.kafka.port
+        # host => "kafka-k8s"
+        # port => 9092
+
+    def _on_kafka_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need kafka relation")
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
+"""
+
+from typing import Optional
+
+from ops.charm import CharmBase, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+LIBID = "eacc8c85082347c9aae740e0220b8376"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+
+KAFKA_HOST_APP_KEY = "host"
+KAFKA_PORT_APP_KEY = "port"
+
+
+class _KafkaAvailableEvent(EventBase):
+    """Event emitted when Kafka is available."""
+
+
+class _KafkaBrokenEvent(EventBase):
+    """Event emitted when Kafka relation is broken."""
+
+
+class KafkaEvents(CharmEvents):
+    """Kafka events.
+
+    This class defines the events that Kafka can emit.
+
+    Events:
+        kafka_available (_KafkaAvailableEvent)
+    """
+
+    kafka_available = EventSource(_KafkaAvailableEvent)
+    kafka_broken = EventSource(_KafkaBrokenEvent)
+
+
+class KafkaRequires(Object):
+    """Requires-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    def _on_relation_changed(self, event) -> None:
+        if event.relation.app and all(
+            key in event.relation.data[event.relation.app]
+            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
+        ):
+            self.charm.on.kafka_available.emit()
+
+    def _on_relation_broken(self, _) -> None:
+        self.charm.on.kafka_broken.emit()
+
+    @property
+    def host(self) -> str:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class KafkaProvides(Object):
+    """Provides-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Kafka host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Kafka hostname or IP address.
+            port (int): Kafka port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
+        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
index 1316a4d..e6b2bf7 100755 (executable)
@@ -28,10 +28,10 @@ from pathlib import Path
 from typing import NoReturn, Optional
 from urllib.parse import urlparse
 
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
 from opslib.osm.interfaces.grafana import GrafanaDashboardTarget
-from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.prometheus import PrometheusScrapeTarget
 from opslib.osm.pod import (
     ContainerV3Builder,
@@ -83,13 +83,16 @@ class ConfigModel(ModelValidator):
 
 
 class KafkaExporterCharm(CharmedOsmBase):
+
+    on = KafkaEvents()
+
     def __init__(self, *args) -> NoReturn:
         super().__init__(*args, oci_image="image")
 
         # Provision Kafka relation to exchange information
-        self.kafka_client = KafkaClient(self, "kafka")
-        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(self.on.kafka_available, self.configure_pod)
+        self.framework.observe(self.on.kafka_broken, self.configure_pod)
 
         # Register relation to provide a Scraping Target
         self.scrape_target = PrometheusScrapeTarget(self, "prometheus-scrape")
@@ -152,10 +155,7 @@ class KafkaExporterCharm(CharmedOsmBase):
         """
         missing_relations = []
 
-        if (
-            self.kafka_client.is_missing_data_in_unit()
-            and self.kafka_client.is_missing_data_in_app()
-        ):
+        if not self.kafka.host or not self.kafka.port:
             missing_relations.append("kafka")
 
         if missing_relations:
@@ -208,7 +208,7 @@ class KafkaExporterCharm(CharmedOsmBase):
         container_builder.add_command(
             [
                 "kafka_exporter",
-                f"--kafka.server={self.kafka_client.host}:{self.kafka_client.port}",
+                f"--kafka.server={self.kafka.host}:{self.kafka.port}",
             ]
         )
         container = container_builder.build()
index 3f266fe..c00943b 100644 (file)
@@ -87,7 +87,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
 
index 8e3318a..f3c9144 100644 (file)
@@ -29,8 +29,10 @@ toxworkdir = /tmp/.tox
 
 [testenv]
 basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
+setenv =
+  VIRTUAL_ENV={envdir}
+  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
+  PYTHONDONTWRITEBYTECODE = 1
 deps =  -r{toxinidir}/requirements.txt
 
 
@@ -99,7 +101,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
index a959d36..88fd16a 100644 (file)
@@ -99,7 +99,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
diff --git a/installers/charm/lcm/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/lcm/lib/charms/kafka_k8s/v0/kafka.py
new file mode 100644 (file)
index 0000000..1baf9a8
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""Kafka library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`kafka` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
+
+Any Charmed Operator that *requires* Kafka for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Kafka would look like
+
+```
+$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  kafka:
+    interface: kafka
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = KafkaEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(
+            self.on.kafka_available,
+            self._on_kafka_available,
+        )
+        self.framework.observe(
+            self.on.kafka_broken,
+            self._on_kafka_broken,
+        )
+
+    def _on_kafka_available(self, event):
+        # Get Kafka host and port
+        host: str = self.kafka.host
+        port: int = self.kafka.port
+        # host => "kafka-k8s"
+        # port => 9092
+
+    def _on_kafka_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need kafka relation")
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
+"""
+
+from typing import Optional
+
+from ops.charm import CharmBase, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+LIBID = "eacc8c85082347c9aae740e0220b8376"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+
+KAFKA_HOST_APP_KEY = "host"
+KAFKA_PORT_APP_KEY = "port"
+
+
+class _KafkaAvailableEvent(EventBase):
+    """Event emitted when Kafka is available."""
+
+
+class _KafkaBrokenEvent(EventBase):
+    """Event emitted when Kafka relation is broken."""
+
+
+class KafkaEvents(CharmEvents):
+    """Kafka events.
+
+    This class defines the events that Kafka can emit.
+
+    Events:
+        kafka_available (_KafkaAvailableEvent)
+    """
+
+    kafka_available = EventSource(_KafkaAvailableEvent)
+    kafka_broken = EventSource(_KafkaBrokenEvent)
+
+
+class KafkaRequires(Object):
+    """Requires-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    def _on_relation_changed(self, event) -> None:
+        if event.relation.app and all(
+            key in event.relation.data[event.relation.app]
+            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
+        ):
+            self.charm.on.kafka_available.emit()
+
+    def _on_relation_broken(self, _) -> None:
+        self.charm.on.kafka_broken.emit()
+
+    @property
+    def host(self) -> str:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class KafkaProvides(Object):
+    """Provides-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Kafka host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Kafka hostname or IP address.
+            port (int): Kafka port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
+        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
index 4a10f5c..16e6f89 100755 (executable)
@@ -27,10 +27,10 @@ import logging
 from typing import NoReturn, Optional
 
 
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
 from opslib.osm.interfaces.http import HttpClient
-from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.pod import ContainerV3Builder, PodRestartPolicy, PodSpecV3Builder
 from opslib.osm.validator import ModelValidator, validator
@@ -140,6 +140,9 @@ class ConfigModel(ModelValidator):
 
 
 class LcmCharm(CharmedOsmBase):
+
+    on = KafkaEvents()
+
     def __init__(self, *args) -> NoReturn:
         super().__init__(
             *args,
@@ -164,9 +167,9 @@ class LcmCharm(CharmedOsmBase):
                     },
                 },
             )
-        self.kafka_client = KafkaClient(self, "kafka")
-        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(self.on.kafka_available, self.configure_pod)
+        self.framework.observe(self.on.kafka_broken, self.configure_pod)
 
         self.mongodb_client = MongoClient(self, "mongodb")
         self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
@@ -179,10 +182,7 @@ class LcmCharm(CharmedOsmBase):
     def _check_missing_dependencies(self, config: ConfigModel):
         missing_relations = []
 
-        if (
-            self.kafka_client.is_missing_data_in_unit()
-            and self.kafka_client.is_missing_data_in_app()
-        ):
+        if not self.kafka.host or not self.kafka.port:
             missing_relations.append("kafka")
         if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
@@ -241,8 +241,8 @@ class LcmCharm(CharmedOsmBase):
                 "OSMLCM_RO_TENANT": "osm",
                 # Kafka configuration
                 "OSMLCM_MESSAGE_DRIVER": "kafka",
-                "OSMLCM_MESSAGE_HOST": self.kafka_client.host,
-                "OSMLCM_MESSAGE_PORT": self.kafka_client.port,
+                "OSMLCM_MESSAGE_HOST": self.kafka.host,
+                "OSMLCM_MESSAGE_PORT": self.kafka.port,
                 # Database configuration
                 "OSMLCM_DATABASE_DRIVER": "mongo",
                 # Storage configuration
index 776b384..aa11a74 100644 (file)
@@ -217,7 +217,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
     def initialize_mongo_config(self):
index 8e3318a..f3c9144 100644 (file)
@@ -29,8 +29,10 @@ toxworkdir = /tmp/.tox
 
 [testenv]
 basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
+setenv =
+  VIRTUAL_ENV={envdir}
+  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
+  PYTHONDONTWRITEBYTECODE = 1
 deps =  -r{toxinidir}/requirements.txt
 
 
@@ -99,7 +101,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
index 216718d..6ab0df6 100644 (file)
 #     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.
-description: Single instance OSM bundle
+name: osm
 bundle: kubernetes
+description: Local bundle for development
 applications:
   zookeeper:
-    charm: "./zookeeper/zookeeper.charm"
+    charm: zookeeper-k8s
+    channel: latest/edge
     scale: 1
-    series: kubernetes
     storage:
-      database: 100M
-    resources:
-      image: rocks.canonical.com:443/k8s.gcr.io/kubernetes-zookeeper:1.0-3.4.10
+      data: 100M
     annotations:
       gui-x: 0
-      gui-y: 550
-  mariadb-k8s:
-    charm: "cs:~charmed-osm/mariadb-k8s"
-    channel: "stable"
+      gui-y: 500
+  mariadb:
+    charm: charmed-osm-mariadb-k8s
     scale: 1
     series: kubernetes
     storage:
@@ -37,21 +35,21 @@ applications:
       root_password: osm4u
       user: mano
     annotations:
-      gui-x: -250
-      gui-y: -200
+      gui-x: -300
+      gui-y: -250
   kafka:
-    charm: "./kafka/kafka.charm"
+    charm: kafka-k8s
+    channel: latest/edge
     scale: 1
-    series: kubernetes
+    trust: true
     storage:
-      database: 100M
-    resources:
-      image: rocks.canonical.com:443/wurstmeister/kafka:2.12-2.2.1
+      data: 100M
     annotations:
       gui-x: 0
-      gui-y: 300
+      gui-y: 250
   mongodb:
-    charm: ch:mongodb-k8s
+    charm: mongodb-k8s
+    channel: latest/stable
     scale: 1
     series: kubernetes
     storage:
@@ -60,116 +58,121 @@ applications:
       gui-x: 0
       gui-y: 0
   nbi:
-    charm: "./nbi/nbi.charm"
+    charm: ./nbi/osm-nbi.charm
     scale: 1
+    resources:
+      image: opensourcemano/nbi:testing-daily
     series: kubernetes
     options:
       database_commonkey: osm
       auth_backend: keystone
-    resources:
-      image: opensourcemano/nbi:testing-daily
+      log_level: DEBUG
     annotations:
       gui-x: 0
-      gui-y: -200
+      gui-y: -250
   ro:
-    charm: "./ro/ro.charm"
+    charm: ./ro/osm-ro.charm
     scale: 1
-    series: kubernetes
     resources:
       image: opensourcemano/ro:testing-daily
+    series: kubernetes
+    options:
+      log_level: DEBUG
     annotations:
-      gui-x: -250
-      gui-y: 300
+      gui-x: -300
+      gui-y: 250
   ng-ui:
-    charm: "./ng-ui/ng-ui.charm"
+    charm: ./ng-ui/osm-ng-ui.charm
     scale: 1
-    series: kubernetes
     resources:
       image: opensourcemano/ng-ui:testing-daily
+    series: kubernetes
     annotations:
-      gui-x: 500
-      gui-y: 100
+      gui-x: 600
+      gui-y: 0
   lcm:
-    charm: "./lcm/lcm.charm"
+    charm: ./lcm/osm-lcm.charm
     scale: 1
+    resources:
+      image: opensourcemano/lcm:testing-daily
     series: kubernetes
     options:
       database_commonkey: osm
-    resources:
-      image: opensourcemano/lcm:testing-daily
+      log_level: DEBUG
     annotations:
-      gui-x: -250
-      gui-y: 50
+      gui-x: -300
+      gui-y: 0
   mon:
-    charm: "./mon/mon.charm"
+    charm: ./mon/osm-mon.charm
     scale: 1
+    resources:
+      image: opensourcemano/mon:testing-daily
     series: kubernetes
     options:
       database_commonkey: osm
-    resources:
-      image: opensourcemano/mon:testing-daily
+      log_level: DEBUG
+      keystone_enabled: true
     annotations:
-      gui-x: 250
-      gui-y: 50
+      gui-x: 300
+      gui-y: 0
   pol:
-    charm: "./pol/pol.charm"
+    charm: ./pol/osm-pol.charm
     scale: 1
-    series: kubernetes
     resources:
       image: opensourcemano/pol:testing-daily
+    series: kubernetes
+    options:
+      log_level: DEBUG
     annotations:
-      gui-x: -250
-      gui-y: 550
+      gui-x: -300
+      gui-y: 500
   pla:
-    charm: "./pla/pla.charm"
+    charm: ./pla/osm-pla.charm
     scale: 1
-    series: kubernetes
     resources:
       image: opensourcemano/pla:testing-daily
+    series: kubernetes
+    options:
+      log_level: DEBUG
     annotations:
-      gui-x: 500
-      gui-y: -200
+      gui-x: 600
+      gui-y: -250
   prometheus:
-    charm: "./prometheus/prometheus.charm"
-    channel: "stable"
+    charm: osm-prometheus
+    channel: latest/edge
     scale: 1
     series: kubernetes
     storage:
       data: 50M
     options:
       default-target: "mon:8000"
-    resources:
-      image: ubuntu/prometheus:latest
-      backup-image: ed1000/prometheus-backup:latest
     annotations:
-      gui-x: 250
-      gui-y: 300
+      gui-x: 300
+      gui-y: 250
   grafana:
-    charm: "./grafana/grafana.charm"
-    channel: "stable"
+    charm: osm-grafana
+    channel: latest/edge
     scale: 1
     series: kubernetes
-    resources:
-      image: ubuntu/grafana:latest
     annotations:
-      gui-x: 250
-      gui-y: 550
+      gui-x: 300
+      gui-y: 500
   keystone:
-    charm: "./keystone/keystone.charm"
+    charm: osm-keystone
+    channel: latest/edge
     resources:
-      image: opensourcemano/keystone:testing-daily
+      keystone-image: opensourcemano/keystone:testing-daily
     scale: 1
-    series: kubernetes
     annotations:
-      gui-x: -250
-      gui-y: 550
+      gui-x: 300
+      gui-y: -250
 relations:
   - - grafana:prometheus
     - prometheus:prometheus
   - - kafka:zookeeper
     - zookeeper:zookeeper
   - - keystone:db
-    - mariadb-k8s:mysql
+    - mariadb:mysql
   - - lcm:kafka
     - kafka:kafka
   - - lcm:mongodb
@@ -206,7 +209,7 @@ relations:
     - nbi:nbi
   - - mon:keystone
     - keystone:keystone
-  - - mariadb-k8s:mysql
+  - - mariadb:mysql
     - pol:mysql
-  - - mariadb-k8s:mysql
-    - grafana:db
+  - - grafana:db
+    - mariadb:mysql
index 0a08eaa..79950ca 100644 (file)
 #     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.
-description: A high-available OSM cluster.
+name: osm-ha
 bundle: kubernetes
+description: Local bundle for development (HA)
 applications:
-  zookeeper-k8s:
-    charm: "cs:~charmed-osm/zookeeper-k8s"
-    channel: "stable"
+  zookeeper:
+    charm: zookeeper-k8s
+    channel: latest/edge
     scale: 3
-    series: kubernetes
     storage:
-      database: 100M
-    options:
-      zookeeper-units: 3
+      data: 100M
     annotations:
       gui-x: 0
-      gui-y: 550
-  mariadb-k8s:
-    charm: "cs:~charmed-osm/mariadb-k8s"
-    channel: "stable"
+      gui-y: 500
+  mariadb:
+    charm: charmed-osm-mariadb-k8s
     scale: 3
     series: kubernetes
     storage:
@@ -39,151 +36,170 @@ applications:
       user: mano
       ha-mode: true
     annotations:
-      gui-x: -250
-      gui-y: -200
-  kafka-k8s:
-    charm: "cs:~charmed-osm/kafka-k8s"
-    channel: "stable"
+      gui-x: -300
+      gui-y: -250
+  kafka:
+    charm: kafka-k8s
+    channel: latest/edge
     scale: 3
-    series: kubernetes
+    trust: true
     storage:
-      database: 100M
-    options:
-      zookeeper-units: 3
-      kafka-units: 3
+      data: 100M
     annotations:
       gui-x: 0
-      gui-y: 300
-  mongodb-k8s:
-    charm: "cs:~charmed-osm/mongodb-k8s"
-    channel: "stable"
+      gui-y: 250
+  mongodb:
+    charm: mongodb-k8s
+    channel: latest/stable
     scale: 3
     series: kubernetes
     storage:
-      database: 50M
-    options:
-      replica-set: rs0
-      namespace: osm
-      enable-sidecar: true
+      db: 50M
     annotations:
       gui-x: 0
-      gui-y: 50
+      gui-y: 0
   nbi:
-    charm: "./nbi/build"
+    charm: ./nbi/osm-nbi.charm
     scale: 3
+    resources:
+      image: opensourcemano/nbi:testing-daily
     series: kubernetes
     options:
       database_commonkey: osm
       auth_backend: keystone
+      log_level: DEBUG
     annotations:
       gui-x: 0
-      gui-y: -200
+      gui-y: -250
   ro:
-    charm: "./ro/build"
+    charm: ./ro/osm-ro.charm
     scale: 3
+    resources:
+      image: opensourcemano/ro:testing-daily
     series: kubernetes
+    options:
+      log_level: DEBUG
     annotations:
-      gui-x: -250
-      gui-y: 300
+      gui-x: -300
+      gui-y: 250
   ng-ui:
-    charm: "./ng-ui/build"
+    charm: ./ng-ui/osm-ng-ui.charm
     scale: 3
+    resources:
+      image: opensourcemano/ng-ui:testing-daily
     series: kubernetes
     annotations:
-      gui-x: 500
-      gui-y: 100
+      gui-x: 600
+      gui-y: 0
   lcm:
-    charm: "./lcm/build"
+    charm: ./lcm/osm-lcm.charm
     scale: 3
+    resources:
+      image: opensourcemano/lcm:testing-daily
     series: kubernetes
     options:
       database_commonkey: osm
+      log_level: DEBUG
     annotations:
-      gui-x: -250
-      gui-y: 50
+      gui-x: -300
+      gui-y: 0
   mon:
-    charm: "./mon/build"
-    scale: 1
+    charm: ./mon/osm-mon.charm
+    scale: 3
+    resources:
+      image: opensourcemano/mon:testing-daily
     series: kubernetes
     options:
       database_commonkey: osm
+      log_level: DEBUG
+      keystone_enabled: true
     annotations:
-      gui-x: 250
-      gui-y: 50
+      gui-x: 300
+      gui-y: 0
   pol:
-    charm: "./pol/build"
+    charm: ./pol/osm-pol.charm
     scale: 3
+    resources:
+      image: opensourcemano/pol:testing-daily
     series: kubernetes
+    options:
+      log_level: DEBUG
     annotations:
-      gui-x: -250
-      gui-y: 550
+      gui-x: -300
+      gui-y: 500
   pla:
-    charm: "./pla/build"
+    charm: ./pla/osm-pla.charm
     scale: 3
+    resources:
+      image: opensourcemano/pla:testing-daily
     series: kubernetes
+    options:
+      log_level: DEBUG
     annotations:
-      gui-x: 500
-      gui-y: -200
+      gui-x: 600
+      gui-y: -250
   prometheus:
-    charm: "./prometheus/build"
-    channel: "stable"
-    scale: 1
+    charm: osm-prometheus
+    channel: latest/edge
+    scale: 3
     series: kubernetes
     storage:
       data: 50M
     options:
       default-target: "mon:8000"
     annotations:
-      gui-x: 250
-      gui-y: 300
+      gui-x: 300
+      gui-y: 250
   grafana:
-    charm: "./grafana/build"
-    channel: "stable"
+    charm: osm-grafana
+    channel: latest/edge
     scale: 3
     series: kubernetes
     annotations:
-      gui-x: 250
-      gui-y: 550
+      gui-x: 300
+      gui-y: 500
   keystone:
-    charm: "./keystone/build"
-    scale: 3
-    series: kubernetes
+    charm: osm-keystone
+    channel: latest/edge
+    resources:
+      keystone-image: opensourcemano/keystone:testing-daily
+    scale: 1
     annotations:
-      gui-x: -250
-      gui-y: 550
+      gui-x: 300
+      gui-y: -250
 relations:
   - - grafana:prometheus
     - prometheus:prometheus
-  - - kafka-k8s:zookeeper
-    - zookeeper-k8s:zookeeper
+  - - kafka:zookeeper
+    - zookeeper:zookeeper
   - - keystone:db
-    - mariadb-k8s:mysql
+    - mariadb:mysql
   - - lcm:kafka
-    - kafka-k8s:kafka
+    - kafka:kafka
   - - lcm:mongodb
-    - mongodb-k8s:mongo
+    - mongodb:database
   - - ro:ro
     - lcm:ro
   - - ro:kafka
-    - kafka-k8s:kafka
+    - kafka:kafka
   - - ro:mongodb
-    - mongodb-k8s:mongo
+    - mongodb:database
   - - pol:kafka
-    - kafka-k8s:kafka
+    - kafka:kafka
   - - pol:mongodb
-    - mongodb-k8s:mongo
+    - mongodb:database
   - - mon:mongodb
-    - mongodb-k8s:mongo
+    - mongodb:database
   - - mon:kafka
-    - kafka-k8s:kafka
+    - kafka:kafka
   - - pla:kafka
-    - kafka-k8s:kafka
+    - kafka:kafka
   - - pla:mongodb
-    - mongodb-k8s:mongo
+    - mongodb:database
   - - nbi:mongodb
-    - mongodb-k8s:mongo
+    - mongodb:database
   - - nbi:kafka
-    - kafka-k8s:kafka
+    - kafka:kafka
   - - nbi:prometheus
     - prometheus:prometheus
   - - nbi:keystone
@@ -192,3 +208,9 @@ relations:
     - prometheus:prometheus
   - - ng-ui:nbi
     - nbi:nbi
+  - - mon:keystone
+    - keystone:keystone
+  - - mariadb:mysql
+    - pol:mysql
+  - - grafana:db
+    - mariadb:mysql
diff --git a/installers/charm/mon/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/mon/lib/charms/kafka_k8s/v0/kafka.py
new file mode 100644 (file)
index 0000000..1baf9a8
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""Kafka library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`kafka` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
+
+Any Charmed Operator that *requires* Kafka for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Kafka would look like
+
+```
+$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  kafka:
+    interface: kafka
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = KafkaEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(
+            self.on.kafka_available,
+            self._on_kafka_available,
+        )
+        self.framework.observe(
+            self.on.kafka_broken,
+            self._on_kafka_broken,
+        )
+
+    def _on_kafka_available(self, event):
+        # Get Kafka host and port
+        host: str = self.kafka.host
+        port: int = self.kafka.port
+        # host => "kafka-k8s"
+        # port => 9092
+
+    def _on_kafka_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need kafka relation")
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
+"""
+
+from typing import Optional
+
+from ops.charm import CharmBase, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+LIBID = "eacc8c85082347c9aae740e0220b8376"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+
+KAFKA_HOST_APP_KEY = "host"
+KAFKA_PORT_APP_KEY = "port"
+
+
+class _KafkaAvailableEvent(EventBase):
+    """Event emitted when Kafka is available."""
+
+
+class _KafkaBrokenEvent(EventBase):
+    """Event emitted when Kafka relation is broken."""
+
+
+class KafkaEvents(CharmEvents):
+    """Kafka events.
+
+    This class defines the events that Kafka can emit.
+
+    Events:
+        kafka_available (_KafkaAvailableEvent)
+    """
+
+    kafka_available = EventSource(_KafkaAvailableEvent)
+    kafka_broken = EventSource(_KafkaBrokenEvent)
+
+
+class KafkaRequires(Object):
+    """Requires-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    def _on_relation_changed(self, event) -> None:
+        if event.relation.app and all(
+            key in event.relation.data[event.relation.app]
+            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
+        ):
+            self.charm.on.kafka_available.emit()
+
+    def _on_relation_broken(self, _) -> None:
+        self.charm.on.kafka_broken.emit()
+
+    @property
+    def host(self) -> str:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class KafkaProvides(Object):
+    """Provides-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Kafka host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Kafka hostname or IP address.
+            port (int): Kafka port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
+        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
index 7833deb..d04779a 100755 (executable)
@@ -28,9 +28,9 @@ import logging
 from typing import NoReturn, Optional
 
 
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.keystone import KeystoneClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.interfaces.prometheus import PrometheusClient
@@ -125,6 +125,9 @@ class ConfigModel(ModelValidator):
 
 
 class MonCharm(CharmedOsmBase):
+
+    on = KafkaEvents()
+
     def __init__(self, *args) -> NoReturn:
         super().__init__(
             *args,
@@ -149,9 +152,9 @@ class MonCharm(CharmedOsmBase):
                     },
                 },
             )
-        self.kafka_client = KafkaClient(self, "kafka")
-        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(self.on.kafka_available, self.configure_pod)
+        self.framework.observe(self.on.kafka_broken, self.configure_pod)
 
         self.mongodb_client = MongoClient(self, "mongodb")
         self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
@@ -172,10 +175,7 @@ class MonCharm(CharmedOsmBase):
     def _check_missing_dependencies(self, config: ConfigModel):
         missing_relations = []
 
-        if (
-            self.kafka_client.is_missing_data_in_unit()
-            and self.kafka_client.is_missing_data_in_app()
-        ):
+        if not self.kafka.host or not self.kafka.port:
             missing_relations.append("kafka")
         if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
@@ -270,8 +270,8 @@ class MonCharm(CharmedOsmBase):
                 "OSMMON_EVALUATOR_INTERVAL": config.evaluator_interval,
                 # Kafka configuration
                 "OSMMON_MESSAGE_DRIVER": "kafka",
-                "OSMMON_MESSAGE_HOST": self.kafka_client.host,
-                "OSMMON_MESSAGE_PORT": self.kafka_client.port,
+                "OSMMON_MESSAGE_HOST": self.kafka.host,
+                "OSMMON_MESSAGE_PORT": self.kafka.port,
                 # Database configuration
                 "OSMMON_DATABASE_DRIVER": "mongo",
                 # Prometheus configuration
index a7cef20..e9748d3 100644 (file)
@@ -148,7 +148,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
     def initialize_mongo_config(self):
index 8e3318a..f3c9144 100644 (file)
@@ -29,8 +29,10 @@ toxworkdir = /tmp/.tox
 
 [testenv]
 basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
+setenv =
+  VIRTUAL_ENV={envdir}
+  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
+  PYTHONDONTWRITEBYTECODE = 1
 deps =  -r{toxinidir}/requirements.txt
 
 
@@ -99,7 +101,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
index 8e3318a..4c7970d 100644 (file)
@@ -99,7 +99,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
diff --git a/installers/charm/nbi/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/nbi/lib/charms/kafka_k8s/v0/kafka.py
new file mode 100644 (file)
index 0000000..1baf9a8
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""Kafka library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`kafka` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
+
+Any Charmed Operator that *requires* Kafka for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Kafka would look like
+
+```
+$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  kafka:
+    interface: kafka
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = KafkaEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(
+            self.on.kafka_available,
+            self._on_kafka_available,
+        )
+        self.framework.observe(
+            self.on.kafka_broken,
+            self._on_kafka_broken,
+        )
+
+    def _on_kafka_available(self, event):
+        # Get Kafka host and port
+        host: str = self.kafka.host
+        port: int = self.kafka.port
+        # host => "kafka-k8s"
+        # port => 9092
+
+    def _on_kafka_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need kafka relation")
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
+"""
+
+from typing import Optional
+
+from ops.charm import CharmBase, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+LIBID = "eacc8c85082347c9aae740e0220b8376"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+
+KAFKA_HOST_APP_KEY = "host"
+KAFKA_PORT_APP_KEY = "port"
+
+
+class _KafkaAvailableEvent(EventBase):
+    """Event emitted when Kafka is available."""
+
+
+class _KafkaBrokenEvent(EventBase):
+    """Event emitted when Kafka relation is broken."""
+
+
+class KafkaEvents(CharmEvents):
+    """Kafka events.
+
+    This class defines the events that Kafka can emit.
+
+    Events:
+        kafka_available (_KafkaAvailableEvent)
+    """
+
+    kafka_available = EventSource(_KafkaAvailableEvent)
+    kafka_broken = EventSource(_KafkaBrokenEvent)
+
+
+class KafkaRequires(Object):
+    """Requires-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    def _on_relation_changed(self, event) -> None:
+        if event.relation.app and all(
+            key in event.relation.data[event.relation.app]
+            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
+        ):
+            self.charm.on.kafka_available.emit()
+
+    def _on_relation_broken(self, _) -> None:
+        self.charm.on.kafka_broken.emit()
+
+    @property
+    def host(self) -> str:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class KafkaProvides(Object):
+    """Provides-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Kafka host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Kafka hostname or IP address.
+            port (int): Kafka port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
+        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
index faba78a..4aaecb9 100755 (executable)
@@ -29,10 +29,10 @@ from typing import NoReturn, Optional
 from urllib.parse import urlparse
 
 
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
 from opslib.osm.interfaces.http import HttpServer
-from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.keystone import KeystoneClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.interfaces.prometheus import PrometheusClient
@@ -118,6 +118,9 @@ class ConfigModel(ModelValidator):
 
 
 class NbiCharm(CharmedOsmBase):
+
+    on = KafkaEvents()
+
     def __init__(self, *args) -> NoReturn:
         super().__init__(
             *args,
@@ -139,9 +142,9 @@ class NbiCharm(CharmedOsmBase):
                 },
             )
 
-        self.kafka_client = KafkaClient(self, "kafka")
-        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(self.on.kafka_available, self.configure_pod)
+        self.framework.observe(self.on.kafka_broken, self.configure_pod)
 
         self.mongodb_client = MongoClient(self, "mongodb")
         self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
@@ -174,10 +177,7 @@ class NbiCharm(CharmedOsmBase):
     def _check_missing_dependencies(self, config: ConfigModel):
         missing_relations = []
 
-        if (
-            self.kafka_client.is_missing_data_in_unit()
-            and self.kafka_client.is_missing_data_in_app()
-        ):
+        if not self.kafka.host or not self.kafka.port:
             missing_relations.append("kafka")
         if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
@@ -227,7 +227,7 @@ class NbiCharm(CharmedOsmBase):
                 "command": [
                     "sh",
                     "-c",
-                    f"until (nc -zvw1 {self.kafka_client.host} {self.kafka_client.port} ); do sleep 3; done; exit 0",
+                    f"until (nc -zvw1 {self.kafka.host} {self.kafka.port} ); do sleep 3; done; exit 0",
                 ],
             }
         )
@@ -257,9 +257,9 @@ class NbiCharm(CharmedOsmBase):
                 "OSMNBI_SERVER_ENABLE_TEST": config.enable_test,
                 "OSMNBI_STATIC_DIR": "/app/osm_nbi/html_public",
                 # Kafka configuration
-                "OSMNBI_MESSAGE_HOST": self.kafka_client.host,
+                "OSMNBI_MESSAGE_HOST": self.kafka.host,
                 "OSMNBI_MESSAGE_DRIVER": "kafka",
-                "OSMNBI_MESSAGE_PORT": self.kafka_client.port,
+                "OSMNBI_MESSAGE_PORT": self.kafka.port,
                 # Database configuration
                 "OSMNBI_DATABASE_DRIVER": "mongo",
                 # Storage configuration
index 116c06b..92c2980 100644 (file)
@@ -159,7 +159,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
     def initialize_mongo_config(self):
index 8e3318a..f3c9144 100644 (file)
@@ -29,8 +29,10 @@ toxworkdir = /tmp/.tox
 
 [testenv]
 basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
+setenv =
+  VIRTUAL_ENV={envdir}
+  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
+  PYTHONDONTWRITEBYTECODE = 1
 deps =  -r{toxinidir}/requirements.txt
 
 
@@ -99,7 +101,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
index ab5c86d..58e13a6 100644 (file)
@@ -99,7 +99,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
diff --git a/installers/charm/pla/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/pla/lib/charms/kafka_k8s/v0/kafka.py
new file mode 100644 (file)
index 0000000..1baf9a8
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""Kafka library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`kafka` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
+
+Any Charmed Operator that *requires* Kafka for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Kafka would look like
+
+```
+$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  kafka:
+    interface: kafka
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = KafkaEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(
+            self.on.kafka_available,
+            self._on_kafka_available,
+        )
+        self.framework.observe(
+            self.on.kafka_broken,
+            self._on_kafka_broken,
+        )
+
+    def _on_kafka_available(self, event):
+        # Get Kafka host and port
+        host: str = self.kafka.host
+        port: int = self.kafka.port
+        # host => "kafka-k8s"
+        # port => 9092
+
+    def _on_kafka_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need kafka relation")
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
+"""
+
+from typing import Optional
+
+from ops.charm import CharmBase, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+LIBID = "eacc8c85082347c9aae740e0220b8376"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+
+KAFKA_HOST_APP_KEY = "host"
+KAFKA_PORT_APP_KEY = "port"
+
+
+class _KafkaAvailableEvent(EventBase):
+    """Event emitted when Kafka is available."""
+
+
+class _KafkaBrokenEvent(EventBase):
+    """Event emitted when Kafka relation is broken."""
+
+
+class KafkaEvents(CharmEvents):
+    """Kafka events.
+
+    This class defines the events that Kafka can emit.
+
+    Events:
+        kafka_available (_KafkaAvailableEvent)
+    """
+
+    kafka_available = EventSource(_KafkaAvailableEvent)
+    kafka_broken = EventSource(_KafkaBrokenEvent)
+
+
+class KafkaRequires(Object):
+    """Requires-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    def _on_relation_changed(self, event) -> None:
+        if event.relation.app and all(
+            key in event.relation.data[event.relation.app]
+            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
+        ):
+            self.charm.on.kafka_available.emit()
+
+    def _on_relation_broken(self, _) -> None:
+        self.charm.on.kafka_broken.emit()
+
+    @property
+    def host(self) -> str:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class KafkaProvides(Object):
+    """Provides-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Kafka host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Kafka hostname or IP address.
+            port (int): Kafka port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
+        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
index 3238dde..2663763 100755 (executable)
@@ -26,9 +26,9 @@
 import logging
 from typing import NoReturn, Optional
 
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.pod import (
     ContainerV3Builder,
@@ -76,12 +76,15 @@ class ConfigModel(ModelValidator):
 
 
 class PlaCharm(CharmedOsmBase):
+
+    on = KafkaEvents()
+
     def __init__(self, *args) -> NoReturn:
         super().__init__(*args, oci_image="image")
 
-        self.kafka_client = KafkaClient(self, "kafka")
-        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(self.on.kafka_available, self.configure_pod)
+        self.framework.observe(self.on.kafka_broken, self.configure_pod)
 
         self.mongodb_client = MongoClient(self, "mongodb")
         self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
@@ -90,10 +93,7 @@ class PlaCharm(CharmedOsmBase):
     def _check_missing_dependencies(self, config: ConfigModel):
         missing_relations = []
 
-        if (
-            self.kafka_client.is_missing_data_in_unit()
-            and self.kafka_client.is_missing_data_in_app()
-        ):
+        if not self.kafka.host or not self.kafka.port:
             missing_relations.append("kafka")
         if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
@@ -141,8 +141,8 @@ class PlaCharm(CharmedOsmBase):
                 "OSMPLA_GLOBAL_LOG_LEVEL": config.log_level,
                 # Kafka configuration
                 "OSMPLA_MESSAGE_DRIVER": "kafka",
-                "OSMPLA_MESSAGE_HOST": self.kafka_client.host,
-                "OSMPLA_MESSAGE_PORT": self.kafka_client.port,
+                "OSMPLA_MESSAGE_HOST": self.kafka.host,
+                "OSMPLA_MESSAGE_PORT": self.kafka.port,
                 # Database configuration
                 "OSMPLA_DATABASE_DRIVER": "mongo",
             }
index debc378..d577e9f 100644 (file)
@@ -102,7 +102,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
     def initialize_mongo_config(self):
index 8e3318a..f3c9144 100644 (file)
@@ -29,8 +29,10 @@ toxworkdir = /tmp/.tox
 
 [testenv]
 basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
+setenv =
+  VIRTUAL_ENV={envdir}
+  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
+  PYTHONDONTWRITEBYTECODE = 1
 deps =  -r{toxinidir}/requirements.txt
 
 
@@ -99,7 +101,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
diff --git a/installers/charm/pol/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/pol/lib/charms/kafka_k8s/v0/kafka.py
new file mode 100644 (file)
index 0000000..1baf9a8
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""Kafka library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`kafka` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
+
+Any Charmed Operator that *requires* Kafka for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Kafka would look like
+
+```
+$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  kafka:
+    interface: kafka
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = KafkaEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(
+            self.on.kafka_available,
+            self._on_kafka_available,
+        )
+        self.framework.observe(
+            self.on.kafka_broken,
+            self._on_kafka_broken,
+        )
+
+    def _on_kafka_available(self, event):
+        # Get Kafka host and port
+        host: str = self.kafka.host
+        port: int = self.kafka.port
+        # host => "kafka-k8s"
+        # port => 9092
+
+    def _on_kafka_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need kafka relation")
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
+"""
+
+from typing import Optional
+
+from ops.charm import CharmBase, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+LIBID = "eacc8c85082347c9aae740e0220b8376"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+
+KAFKA_HOST_APP_KEY = "host"
+KAFKA_PORT_APP_KEY = "port"
+
+
+class _KafkaAvailableEvent(EventBase):
+    """Event emitted when Kafka is available."""
+
+
+class _KafkaBrokenEvent(EventBase):
+    """Event emitted when Kafka relation is broken."""
+
+
+class KafkaEvents(CharmEvents):
+    """Kafka events.
+
+    This class defines the events that Kafka can emit.
+
+    Events:
+        kafka_available (_KafkaAvailableEvent)
+    """
+
+    kafka_available = EventSource(_KafkaAvailableEvent)
+    kafka_broken = EventSource(_KafkaBrokenEvent)
+
+
+class KafkaRequires(Object):
+    """Requires-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    def _on_relation_changed(self, event) -> None:
+        if event.relation.app and all(
+            key in event.relation.data[event.relation.app]
+            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
+        ):
+            self.charm.on.kafka_available.emit()
+
+    def _on_relation_broken(self, _) -> None:
+        self.charm.on.kafka_broken.emit()
+
+    @property
+    def host(self) -> str:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class KafkaProvides(Object):
+    """Provides-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Kafka host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Kafka hostname or IP address.
+            port (int): Kafka port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
+        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
index e2fcdb3..7b92b45 100755 (executable)
@@ -27,9 +27,9 @@ import logging
 import re
 from typing import NoReturn, Optional
 
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.interfaces.mysql import MysqlClient
 from opslib.osm.pod import (
@@ -87,6 +87,9 @@ class ConfigModel(ModelValidator):
 
 
 class PolCharm(CharmedOsmBase):
+
+    on = KafkaEvents()
+
     def __init__(self, *args) -> NoReturn:
         super().__init__(
             *args,
@@ -107,9 +110,9 @@ class PolCharm(CharmedOsmBase):
                     },
                 },
             )
-        self.kafka_client = KafkaClient(self, "kafka")
-        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(self.on.kafka_available, self.configure_pod)
+        self.framework.observe(self.on.kafka_broken, self.configure_pod)
 
         self.mongodb_client = MongoClient(self, "mongodb")
         self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
@@ -122,10 +125,7 @@ class PolCharm(CharmedOsmBase):
     def _check_missing_dependencies(self, config: ConfigModel):
         missing_relations = []
 
-        if (
-            self.kafka_client.is_missing_data_in_unit()
-            and self.kafka_client.is_missing_data_in_app()
-        ):
+        if not self.kafka.host or not self.kafka.port:
             missing_relations.append("kafka")
         if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
@@ -185,8 +185,8 @@ class PolCharm(CharmedOsmBase):
                 "OSMPOL_GLOBAL_LOGLEVEL": config.log_level,
                 # Kafka configuration
                 "OSMPOL_MESSAGE_DRIVER": "kafka",
-                "OSMPOL_MESSAGE_HOST": self.kafka_client.host,
-                "OSMPOL_MESSAGE_PORT": self.kafka_client.port,
+                "OSMPOL_MESSAGE_HOST": self.kafka.host,
+                "OSMPOL_MESSAGE_PORT": self.kafka.port,
                 # Database configuration
                 "OSMPOL_DATABASE_DRIVER": "mongo",
             }
index 330ecee..6cf435d 100644 (file)
@@ -132,7 +132,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
     def initialize_mongo_config(self):
index 8e3318a..f3c9144 100644 (file)
@@ -29,8 +29,10 @@ toxworkdir = /tmp/.tox
 
 [testenv]
 basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
+setenv =
+  VIRTUAL_ENV={envdir}
+  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
+  PYTHONDONTWRITEBYTECODE = 1
 deps =  -r{toxinidir}/requirements.txt
 
 
@@ -99,7 +101,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
index 8e3318a..4c7970d 100644 (file)
@@ -99,7 +99,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
diff --git a/installers/charm/ro/lib/charms/kafka_k8s/v0/kafka.py b/installers/charm/ro/lib/charms/kafka_k8s/v0/kafka.py
new file mode 100644 (file)
index 0000000..1baf9a8
--- /dev/null
@@ -0,0 +1,207 @@
+# Copyright 2022 Canonical Ltd.
+# See LICENSE file for licensing details.
+#
+# 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.
+
+"""Kafka library.
+
+This [library](https://juju.is/docs/sdk/libraries) implements both sides of the
+`kafka` [interface](https://juju.is/docs/sdk/relations).
+
+The *provider* side of this interface is implemented by the
+[kafka-k8s Charmed Operator](https://charmhub.io/kafka-k8s).
+
+Any Charmed Operator that *requires* Kafka for providing its
+service should implement the *requirer* side of this interface.
+
+In a nutshell using this library to implement a Charmed Operator *requiring*
+Kafka would look like
+
+```
+$ charmcraft fetch-lib charms.kafka_k8s.v0.kafka
+```
+
+`metadata.yaml`:
+
+```
+requires:
+  kafka:
+    interface: kafka
+    limit: 1
+```
+
+`src/charm.py`:
+
+```
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
+from ops.charm import CharmBase
+
+
+class MyCharm(CharmBase):
+
+    on = KafkaEvents()
+
+    def __init__(self, *args):
+        super().__init__(*args)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(
+            self.on.kafka_available,
+            self._on_kafka_available,
+        )
+        self.framework.observe(
+            self.on.kafka_broken,
+            self._on_kafka_broken,
+        )
+
+    def _on_kafka_available(self, event):
+        # Get Kafka host and port
+        host: str = self.kafka.host
+        port: int = self.kafka.port
+        # host => "kafka-k8s"
+        # port => 9092
+
+    def _on_kafka_broken(self, event):
+        # Stop service
+        # ...
+        self.unit.status = BlockedStatus("need kafka relation")
+```
+
+You can file bugs
+[here](https://github.com/charmed-osm/kafka-k8s-operator/issues)!
+"""
+
+from typing import Optional
+
+from ops.charm import CharmBase, CharmEvents
+from ops.framework import EventBase, EventSource, Object
+
+# The unique Charmhub library identifier, never change it
+from ops.model import Relation
+
+LIBID = "eacc8c85082347c9aae740e0220b8376"
+
+# Increment this major API version when introducing breaking changes
+LIBAPI = 0
+
+# Increment this PATCH version before using `charmcraft publish-lib` or reset
+# to 0 if you are raising the major API version
+LIBPATCH = 3
+
+
+KAFKA_HOST_APP_KEY = "host"
+KAFKA_PORT_APP_KEY = "port"
+
+
+class _KafkaAvailableEvent(EventBase):
+    """Event emitted when Kafka is available."""
+
+
+class _KafkaBrokenEvent(EventBase):
+    """Event emitted when Kafka relation is broken."""
+
+
+class KafkaEvents(CharmEvents):
+    """Kafka events.
+
+    This class defines the events that Kafka can emit.
+
+    Events:
+        kafka_available (_KafkaAvailableEvent)
+    """
+
+    kafka_available = EventSource(_KafkaAvailableEvent)
+    kafka_broken = EventSource(_KafkaBrokenEvent)
+
+
+class KafkaRequires(Object):
+    """Requires-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self.charm = charm
+        self._endpoint_name = endpoint_name
+
+        # Observe relation events
+        event_observe_mapping = {
+            charm.on[self._endpoint_name].relation_changed: self._on_relation_changed,
+            charm.on[self._endpoint_name].relation_broken: self._on_relation_broken,
+        }
+        for event, observer in event_observe_mapping.items():
+            self.framework.observe(event, observer)
+
+    def _on_relation_changed(self, event) -> None:
+        if event.relation.app and all(
+            key in event.relation.data[event.relation.app]
+            for key in (KAFKA_HOST_APP_KEY, KAFKA_PORT_APP_KEY)
+        ):
+            self.charm.on.kafka_available.emit()
+
+    def _on_relation_broken(self, _) -> None:
+        self.charm.on.kafka_broken.emit()
+
+    @property
+    def host(self) -> str:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            relation.data[relation.app].get(KAFKA_HOST_APP_KEY)
+            if relation and relation.app
+            else None
+        )
+
+    @property
+    def port(self) -> int:
+        relation: Relation = self.model.get_relation(self._endpoint_name)
+        return (
+            int(relation.data[relation.app].get(KAFKA_PORT_APP_KEY))
+            if relation and relation.app
+            else None
+        )
+
+
+class KafkaProvides(Object):
+    """Provides-side of the Kafka relation."""
+
+    def __init__(self, charm: CharmBase, endpoint_name: str = "kafka") -> None:
+        super().__init__(charm, endpoint_name)
+        self._endpoint_name = endpoint_name
+
+    def set_host_info(self, host: str, port: int, relation: Optional[Relation] = None) -> None:
+        """Set Kafka host and port.
+
+        This function writes in the application data of the relation, therefore,
+        only the unit leader can call it.
+
+        Args:
+            host (str): Kafka hostname or IP address.
+            port (int): Kafka port.
+            relation (Optional[Relation]): Relation to update.
+                                           If not specified, all relations will be updated.
+
+        Raises:
+            Exception: if a non-leader unit calls this function.
+        """
+        if not self.model.unit.is_leader():
+            raise Exception("only the leader set host information.")
+
+        if relation:
+            self._update_relation_data(host, port, relation)
+            return
+
+        for relation in self.model.relations[self._endpoint_name]:
+            self._update_relation_data(host, port, relation)
+
+    def _update_relation_data(self, host: str, port: int, relation: Relation) -> None:
+        """Update data in relation if needed."""
+        relation.data[self.model.app][KAFKA_HOST_APP_KEY] = host
+        relation.data[self.model.app][KAFKA_PORT_APP_KEY] = str(port)
index 1367a44..b196b19 100755 (executable)
@@ -26,9 +26,9 @@ import base64
 import logging
 from typing import Dict, NoReturn, Optional
 
+from charms.kafka_k8s.v0.kafka import KafkaEvents, KafkaRequires
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
-from opslib.osm.interfaces.kafka import KafkaClient
 from opslib.osm.interfaces.mongo import MongoClient
 from opslib.osm.interfaces.mysql import MysqlClient
 from opslib.osm.pod import (
@@ -126,6 +126,8 @@ class ConfigModel(ModelValidator):
 class RoCharm(CharmedOsmBase):
     """GrafanaCharm Charm."""
 
+    on = KafkaEvents()
+
     def __init__(self, *args) -> NoReturn:
         """Prometheus Charm constructor."""
         super().__init__(
@@ -144,9 +146,9 @@ class RoCharm(CharmedOsmBase):
                     **_get_ro_host_paths(self.config.get("debug_ro_local_path")),
                 },
             )
-        self.kafka_client = KafkaClient(self, "kafka")
-        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
-        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
+        self.kafka = KafkaRequires(self)
+        self.framework.observe(self.on.kafka_available, self.configure_pod)
+        self.framework.observe(self.on.kafka_broken, self.configure_pod)
 
         self.mysql_client = MysqlClient(self, "mysql")
         self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
@@ -176,10 +178,7 @@ class RoCharm(CharmedOsmBase):
         missing_relations = []
 
         if config.enable_ng_ro:
-            if (
-                self.kafka_client.is_missing_data_in_unit()
-                and self.kafka_client.is_missing_data_in_app()
-            ):
+            if not self.kafka.host or not self.kafka.port:
                 missing_relations.append("kafka")
             if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
                 missing_relations.append("mongodb")
@@ -286,8 +285,8 @@ class RoCharm(CharmedOsmBase):
             container_builder.add_envs(
                 {
                     "OSMRO_MESSAGE_DRIVER": "kafka",
-                    "OSMRO_MESSAGE_HOST": self.kafka_client.host,
-                    "OSMRO_MESSAGE_PORT": self.kafka_client.port,
+                    "OSMRO_MESSAGE_HOST": self.kafka.host,
+                    "OSMRO_MESSAGE_PORT": self.kafka.port,
                     # MongoDB configuration
                     "OSMRO_DATABASE_DRIVER": "mongo",
                 }
index 2d317bd..f18e768 100644 (file)
@@ -125,7 +125,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
         # Initializing the mongodb config
@@ -143,7 +143,7 @@ class TestCharm(unittest.TestCase):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+            kafka_relation_id, "kafka", {"host": "kafka", "port": 9092}
         )
 
         # Initializing the mongo relation
index 8e3318a..f3c9144 100644 (file)
@@ -29,8 +29,10 @@ toxworkdir = /tmp/.tox
 
 [testenv]
 basepython = python3.8
-setenv = VIRTUAL_ENV={envdir}
-         PYTHONDONTWRITEBYTECODE = 1
+setenv =
+  VIRTUAL_ENV={envdir}
+  PYTHONPATH = {toxinidir}:{toxinidir}/lib:{toxinidir}/src
+  PYTHONDONTWRITEBYTECODE = 1
 deps =  -r{toxinidir}/requirements.txt
 
 
@@ -99,7 +101,7 @@ whitelist_externals =
   charmcraft
   sh
 commands =
-  charmcraft build
+  charmcraft pack
   sh -c 'ubuntu_version=20.04; \
         architectures="amd64-aarch64-arm64"; \
         charm_name=`cat metadata.yaml | grep -E "^name: " | cut -f 2 -d " "`; \
index a2b04aa..d26b9e7 100755 (executable)
@@ -27,15 +27,10 @@ PATH=/snap/bin:${PATH}
 
 MODEL_NAME=osm
 
-# Latest bundles using old mongodb-k8s
-# OSM_BUNDLE=cs:osm-68
-# OSM_HA_BUNDLE=cs:osm-ha-54
-# The charm store does not support referencing charms from CharmHub,
-# therefore we will point to the local bundles until we migrate all
-# charms to CharmHub.
-OSM_BUNDLE=/usr/share/osm-devops/installers/charm/bundles/osm/bundle.yaml
-OSM_HA_BUNDLE=/usr/share/osm-devops/installers/charm/bundles/osm-ha/bundle.yaml
-TAG=testing-daily
+OSM_BUNDLE=ch:osm
+OSM_HA_BUNDLE=ch:osm-ha
+CHARMHUB_CHANNEL=latest/edge
+unset TAG
 
 function check_arguments(){
     while [ $# -gt 0 ] ; do
@@ -263,9 +258,9 @@ function deploy_charmed_osm(){
     fi
 
     if [ -v BUNDLE ]; then
-        juju deploy -m $MODEL_NAME $BUNDLE --overlay ~/.osm/vca-overlay.yaml $images_overlay $extra_overlay
+        juju deploy --trust --channel $CHARMHUB_CHANNEL -m $MODEL_NAME $BUNDLE --overlay ~/.osm/vca-overlay.yaml $images_overlay $extra_overlay
     else
-        juju deploy -m $MODEL_NAME $OSM_BUNDLE --overlay ~/.osm/vca-overlay.yaml $images_overlay $extra_overlay
+        juju deploy --trust --channel $CHARMHUB_CHANNEL -m $MODEL_NAME $OSM_BUNDLE --overlay ~/.osm/vca-overlay.yaml $images_overlay $extra_overlay
     fi
 
     if [ ! -v KUBECFG ]; then