Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3928536
D2940.id7486.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
D2940.id7486.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D2940: Listen to Docker events
Attached
Detach File
Event Timeline
Log In to Comment