Page MenuHomeDevCentral

D2940.id7486.diff
No OneTemporary

D2940.id7486.diff

diff --git a/.arcconfig b/.arcconfig
new file mode 100644
--- /dev/null
+++ b/.arcconfig
@@ -0,0 +1,4 @@
+{
+ "phabricator.uri": "https://devcentral.nasqueron.org/",
+ "repository.callsign": "DOCKERTIDE"
+}
diff --git a/.arclint b/.arclint
new file mode 100644
--- /dev/null
+++ b/.arclint
@@ -0,0 +1,28 @@
+{
+ "linters": {
+ "chmod": {
+ "type": "chmod"
+ },
+ "filename": {
+ "type": "filename"
+ },
+ "json": {
+ "type": "json",
+ "include": [
+ "(^\\.arcconfig$)",
+ "(^\\.arclint$)",
+ "(\\.json$)"
+ ]
+ },
+ "python": {
+ "type": "flake8",
+ "severity": {
+ "E203": "disabled",
+ "F821": "advice"
+ },
+ "include": [
+ "(\\.py$)"
+ ]
+ }
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# Python package build
+dist/
+*.egg-info/
+
+# Python
+__pycache__/
+*.pyc
+*.pyo
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright 2023 Sébastien Santoro aka Dereckson, from Nasqueron.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,43 @@
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+RMDIR=rm -rf
+PYTHON=python
+DISCOVER_TESTS=$(PYTHON) -m unittest discover
+REFORMAT=black
+
+# -------------------------------------------------------------
+# Main targets
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+all:
+
+package: dist
+
+clean: clean-package
+
+test:
+ ${DISCOVER_TESTS} tests/
+
+# -------------------------------------------------------------
+# Development helpers
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+reformat:
+ find bin -type f | xargs ${REFORMAT}
+ find src -type f -name '*.py' | xargs ${REFORMAT}
+ find tests -type f -name '*.py' | xargs ${REFORMAT}
+
+# -------------------------------------------------------------
+# Packaging targets
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+dist:
+ ${PYTHON} -m build
+
+clean-package:
+ ${RMDIR} dist src/dockertide.egg-info
diff --git a/bin/docker-tide b/bin/docker-tide
new file mode 100755
--- /dev/null
+++ b/bin/docker-tide
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+from dockertide import TideConfiguration, Reactor
+
+
+def run():
+ config = TideConfiguration()
+
+ reactor = Reactor(config)
+ reactor.listen_events()
+
+
+if __name__ == "__main__":
+ run()
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1 @@
+unittest-data-provider>=1.0.1,<2.0
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,14 @@
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+[build-system]
+requires = [
+ "setuptools>=42",
+ "wheel"
+]
+
+build-backend = "setuptools.build_meta"
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+docker>=5.0,<6.0
+PyYAML>=6.0,<7.0
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,33 @@
+[metadata]
+name = docker-tide
+version = 0.1.0
+author = Sébastien Santoro
+author_email = dereckson@espace-win.org
+description = Reactor to Docker events
+long_description = file: README.md
+long_description_content_type = text/markdown
+license = BSD-2-Clause
+license_files = LICENSE
+url = https://devcentral.nasqueron.org/source/docker-tide/
+project_urls =
+ Bug Tracker = https://devcentral.nasqueron.org/tag/docker_tide/
+classifiers =
+ Programming Language :: Python :: 3
+ License :: OSI Approved :: BSD License
+ Operating System :: POSIX :: Linux
+ Environment :: Console
+ Intended Audience :: System Administrators
+
+[options]
+package_dir =
+ = src
+packages = find:
+scripts =
+ bin/docker-tide
+python_requires = >=3.6
+install_requires =
+ docker>=5.0,<6.0
+ PyYAML>=6.0,<7.0
+
+[options.packages.find]
+where = src
diff --git a/src/dockertide/__init__.py b/src/dockertide/__init__.py
new file mode 100644
--- /dev/null
+++ b/src/dockertide/__init__.py
@@ -0,0 +1,10 @@
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+from .config import TideConfiguration
+from .reactor import Reactor
diff --git a/src/dockertide/config.py b/src/dockertide/config.py
new file mode 100644
--- /dev/null
+++ b/src/dockertide/config.py
@@ -0,0 +1,50 @@
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import os
+import yaml
+
+from dockertide import matcher
+
+
+DOCKER_TIDE_CONFIGURATION_PATH = "/etc/docker/tide.conf"
+
+
+# -------------------------------------------------------------
+# Parse configuration file
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def read_config():
+ if not os.path.exists(DOCKER_TIDE_CONFIGURATION_PATH):
+ raise RuntimeError(
+ f"Missing configuration file: {DOCKER_TIDE_CONFIGURATION_PATH}"
+ )
+
+ with open(DOCKER_TIDE_CONFIGURATION_PATH) as fd:
+ return yaml.safe_load(fd)
+
+
+# -------------------------------------------------------------
+# Configuration
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+class TideConfiguration:
+ def __init__(self, config=None):
+ if not config:
+ config = read_config()
+
+ self.config = config
+
+ def query_events_reactor(self, event):
+ return {
+ name: args
+ for name, args in self.config.get("events_reactor", {}).items()
+ if matcher.is_event_matches(event, args.get("event", {}))
+ }
diff --git a/src/dockertide/matcher.py b/src/dockertide/matcher.py
new file mode 100644
--- /dev/null
+++ b/src/dockertide/matcher.py
@@ -0,0 +1,73 @@
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+# -------------------------------------------------------------
+# Correspondance between:
+# - our configuration ("template")
+# - Docker events ("event")
+#
+# The dot (.) is used as key hierarchic separator
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+EVENT_TEMPLATE_KEYS = [
+ {"template": "type", "event": "Type"},
+ {"template": "action", "event": "Action"},
+ {"template": "actor.exitCode", "event": "Actor.Attributes.exitCode"},
+ {"template": "actor.image", "event": "Actor.Attributes.image"},
+ {"template": "actor.name", "event": "Actor.Attributes.name"},
+]
+
+
+# -------------------------------------------------------------
+# Events matcher methods
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+def is_event_matches(event, template):
+ for key in EVENT_TEMPLATE_KEYS:
+ if has_key(template, key["template"]):
+ if not are_equals(template, key["template"], event, key["event"]):
+ return False
+
+ return True
+
+
+def has_key(data, key):
+ if "." in key:
+ pos = key.find(".")
+ newkey = key[:pos]
+ if newkey not in data:
+ return False
+
+ return has_key(data[newkey], key[pos + 1 :])
+
+ return key in data
+
+
+def get_value(data, key):
+ if "." in key:
+ pos = key.find(".")
+ return get_value(data[key[:pos]], key[pos + 1 :])
+
+ return data[key]
+
+
+def are_equals(left, left_key, right, right_key):
+ left_value = get_value(left, left_key)
+ right_value = get_value(right, right_key)
+
+ return left_value == right_value
+
+
+def templatize(event, expression):
+ for key in EVENT_TEMPLATE_KEYS:
+ candidate = "%%" + key["template"] + "%%"
+ if candidate in expression:
+ expression = expression.replace(candidate, get_value(event, key["event"]))
+
+ return expression
diff --git a/src/dockertide/reactor.py b/src/dockertide/reactor.py
new file mode 100644
--- /dev/null
+++ b/src/dockertide/reactor.py
@@ -0,0 +1,119 @@
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import json
+import logging
+import os
+import subprocess
+
+import docker
+
+from dockertide import config
+from dockertide import matcher
+
+
+# -------------------------------------------------------------
+# Constants
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+HOSTNAME = os.environ["HOSTNAME"]
+
+
+# -------------------------------------------------------------
+# Logging
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+logger = logging.getLogger("docker-tide")
+logger.setLevel(logging.WARNING)
+
+ch = logging.StreamHandler()
+logger.addHandler(ch)
+
+
+# -------------------------------------------------------------
+# Reactor
+#
+# Listen to events and handle them
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+class Reactor:
+ def __init__(self, config):
+ self.config = config
+ self.docker_client = docker.from_env()
+
+ def handle_event(self, event):
+ logger.debug(f"Event received: {event}")
+ for title, action in self.config.query_events_reactor(event).items():
+ logger.debug(f"Action matches event: {title}")
+ ReactorAction(self.docker_client, event, action).run()
+
+ def listen_events(self):
+ for event in self.docker_client.events(decode=True):
+ self.handle_event(event)
+
+
+# -------------------------------------------------------------
+# Reactor action
+#
+# Listen to events and handle them
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+
+class ReactorAction:
+ def __init__(self, docker_client, event, action):
+ self.docker_client = docker_client
+ self.event = event
+ self.action = action
+
+ def run(self):
+ if "run" in self.action:
+ command = self.templatize(self.action["run"])
+ logger.info(f"Running command: {command}")
+
+ try:
+ process = subprocess.run(command, capture_output=True)
+ except Exception as err:
+ logger.error(f"Can't run command: {err}")
+ return
+
+ if process.returncode != 0:
+ logger.warning(f"Command didn't exit with 0: {process}")
+ else:
+ logger.debug(f"Command successful: {process}")
+
+ def resolve_container_ip(self):
+ if self.event["Type"] != "container":
+ raise RuntimeError("Can only resolve IP for a container event.")
+
+ id = self.event["id"]
+ container = self.docker_client.containers.get(id)
+ networks = container.attrs["NetworkSettings"]["Networks"]
+ for network, args in networks.items():
+ if "IPAddress" in args:
+ return args["IPAddress"]
+
+ return None
+
+ def templatize(self, expression):
+ if type(expression) is list:
+ return [self.templatize(item) for item in expression]
+
+ if "%%host%%" in expression:
+ expression = expression.replace("%%host%%", HOSTNAME)
+
+ if "%%payload%%" in expression:
+ expression = expression.replace("%%payload", json.dumps(self.event))
+
+ if "%%ip%%" in expression:
+ ip = self.resolve_container_ip()
+ expression = expression.replace("%%ip%%", ip)
+
+ return matcher.templatize(self.event, expression)
diff --git a/tests/data/matches.yml b/tests/data/matches.yml
new file mode 100644
--- /dev/null
+++ b/tests/data/matches.yml
@@ -0,0 +1,24 @@
+events_matches:
+
+ - payload:
+ {'status': 'start', 'id': '6512141f47e592678c186042f960f4cba44c073e8805befeaffbb46b01a6091d', 'from': 'nasqueron/bugzilla', 'Type': 'container', 'Action': 'start', 'Actor': {'ID': '6512141f47e592678c186042f960f4cba44c073e8805befeaffbb46b01a6091d', 'Attributes': {'image': 'nasqueron/bugzilla', 'name': 'ew_bugzilla'}}, 'scope': 'local', 'time': 1680003578, 'timeNano': 1680003578648991676}
+ template:
+ type: container
+ action: start
+ actor:
+ name: ew_bugzilla
+ match: True
+
+ - payload:
+ {'status': 'start', 'id': '6512141f47e592678c186042f960f4cba44c073e8805befeaffbb46b01a6091d', 'from': 'nasqueron/bugzilla', 'Type': 'container', 'Action': 'start', 'Actor': {'ID': '6512141f47e592678c186042f960f4cba44c073e8805befeaffbb46b01a6091d', 'Attributes': {'image': 'nasqueron/bugzilla', 'name': 'ew_bugzilla'}}, 'scope': 'local', 'time': 1680003578, 'timeNano': 1680003578648991676}
+ template:
+ type: container
+ action: start
+ actor:
+ name: bogusname
+ match: False
+
+ - payload:
+ {'status': 'start', 'id': '6512141f47e592678c186042f960f4cba44c073e8805befeaffbb46b01a6091d', 'from': 'nasqueron/bugzilla', 'Type': 'container', 'Action': 'start', 'Actor': {'ID': '6512141f47e592678c186042f960f4cba44c073e8805befeaffbb46b01a6091d', 'Attributes': {'image': 'nasqueron/bugzilla', 'name': 'ew_bugzilla'}}, 'scope': 'local', 'time': 1680003578, 'timeNano': 1680003578648991676}
+ template: {}
+ match: True
diff --git a/tests/test_matcher.py b/tests/test_matcher.py
new file mode 100755
--- /dev/null
+++ b/tests/test_matcher.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+
+# -------------------------------------------------------------
+# Docker Tide :: Docker events reactor :: Tests
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+# Project: Nasqueron
+# License: BSD-2-Clause
+# -------------------------------------------------------------
+
+
+import unittest
+
+from unittest_data_provider import data_provider
+import yaml
+
+from dockertide import matcher
+
+
+EVENTS_MATCHES_DATA_PATH = "tests/data/matches.yml"
+
+
+class TestMatcher(unittest.TestCase):
+ def get_event_matches():
+ with open(EVENTS_MATCHES_DATA_PATH) as fd:
+ matches = yaml.safe_load(fd)["events_matches"]
+
+ return ((item["payload"], item["template"], item["match"]) for item in matches)
+
+ @data_provider(get_event_matches)
+ def test_is_event_matches(self, event, template, match):
+ self.assertEqual(match, matcher.is_event_matches(event, template))
+
+
+if __name__ == "__main__":
+ unittest.main()

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 23, 05:25 (8 h, 16 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2311784
Default Alt Text
D2940.id7486.diff (17 KB)

Event Timeline