gordon
: Event-driven Cloud DNS registration¶
Service to consume hostname-related events from a pub/sub and add, update, & delete records for a 3rd party DNS provider.
Release v0.0.1.dev10 (What’s new?).
Warning
This is still in the planning phase and under active development. Gordon should not be used in production, yet.
Requirements¶
For the initial release, the following will be supported:
- Python 3.6
- Google Cloud Platform
Support for other Python versions and cloud providers may be added.
Development¶
For development and running tests, your system must have all supported versions of Python installed. We suggest using pyenv.
Setup¶
$ git clone git@github.com:spotify/gordon.git && cd gordon
# make a virtualenv
(env) $ pip install -r dev-requirements.txt
Running tests¶
To run the entire test suite:
# outside of the virtualenv
# if tox is not yet installed
$ pip install tox
$ tox
If you want to run the test suite for a specific version of Python:
# outside of the virtualenv
$ tox -e py36
To run an individual test, call pytest
directly:
# inside virtualenv
(env) $ pytest tests/test_foo.py
Build docs¶
To generate documentation:
(env) $ pip install -r docs-requirements.txt
(env) $ cd docs && make html # builds HTML files into _build/html/
(env) $ cd _build/html
(env) $ python -m http.server $PORT
Then navigate to localhost:$PORT
!
To watch for changes and automatically reload in the browser:
(env) $ cd docs
(env) $ make livehtml # default port 8888
# to change port
(env) $ make livehtml PORT=8080
Code of Conduct¶
This project adheres to the Open Code of Conduct. By participating, you are expected to honor this code.
User’s Guide¶
Configuring the Gordon Service¶
Main module to run the Gordon service.
The service expects a gordon.toml
and/or a gordon-user.toml
file for configuration in the current working directory, or in a
provided root directory.
Any configuration defined in gordon-user.toml
overwrites those in
gordon.toml
.
Example:
$ python gordon/main.py
$ python gordon/main.py -c /etc/default/
$ python gordon/main.py --config-root /etc/default/
Example Configuration¶
An example of a gordon.toml
file:
# Gordon Core Config
[core]
plugins = ["foo.plugin"]
debug = false
metrics = "ffwd"
[core.route]
consume = "enrich"
enrich = "publish"
publish = "cleanup"
[core.logging]
level = "info"
handlers = ["syslog"]
fmt = "%(created)f %(levelno)d %(message)s"
date_fmt = "%Y-%m-%dT%H:%M:%S"
address = ["10.99.0.1", "514"]
# Plugin Config
["foo"]
# global config to the general "foo" package
bar = baz
["foo.plugin"]
# specific plugin config within "foo" package
baz = bla
You may choose to have a gordon-user.toml
file for development. All tables are deep merged into gordon.toml
, to limit the amount of config duplication needed. For example, you can override core.debug
without having to redeclare which plugins you’d like to run.
[core]
debug = true
[core.logging]
level = "debug"
handlers = ["stream"]
Supported Configuration¶
The following sections are supported:
core¶
-
plugins
=LIST-OF-STRINGS
¶ Plugins that the Gordon service needs to load. If a plugin is not listed, Gordon will skip it even if there’s configuration.
The strings must match the plugin’s config key. See the plugin’s documentation for config key names.
-
debug
=true|false
¶ Whether or not to run the Gordon service in
debug
mode.If
true
, Gordon will continue running even if installed & configured plugins can not be loaded. Plugin exceptions will be logged as warnings with tracebacks.If
false
, Gordon will exit out if it can’t load one or more plugins.
-
metrics
=STR
¶ The metrics provider to use. Depending on the provider, more configuration may be needed. See provider implementation for details.
core.logging¶
-
level
=info(default)|debug|warning|error|critical
¶ Any log level that is supported by the Python standard
logging
library.
-
handlers
=LIST-OF-STRINGS
¶ handlers
support any of the following handlers:stream
,syslog
, andstackdriver
. Multiple handlers are supported. Defaults tosyslog
if none are defined.Note
If
stackdriver
is selected,ulogger[stackdriver]
needs to be installed as its dependencies are not installed by default.
Other key-value pairs as supported by ulogger will be passed into the configured handlers. For example:
[core.logging]
level = "info"
handlers = ["syslog"]
address = ["10.99.0.1", "514"]
fmt = "%(created)f %(levelno)d %(message)s"
date_fmt = "%Y-%m-%dT%H:%M:%S"
core.route¶
A table of key-value pairs of phases used to indicate the route the a message should take. All keys should correspond to either the start_phase attribute of a runnable plugin or the phase of a message handling plugin. Values may only correspond to phase of a message handling plugin.
[core.route]
start_phase = "phase2"
phase2 = "phase3"
Gordon’s Plugin System¶
Module for loading plugins distributed via third-party packages.
Plugin discovery is done via entry_points
defined in a package’s
setup.py
, registered under 'gordon.plugins'
. For example:
# setup.py
from setuptools import setup
setup(
name=NAME,
# snip
entry_points={
'gordon.plugins': [
'gcp.gpubsub = gordon_gcp.gpubsub:EventClient',
'gcp.gce.a = gordon_gcp.gce.a:ReferenceSourceClient',
'gcp.gce.b = gordon_gcp.gce.b:ReferenceSourceClient',
'gcp.gdns = gordon_gcp.gdns:DNSProviderClient',
],
},
# snip
)
Plugins are initialized with any config defined in gordon-user.toml
and gordon.toml
. See Configuring the Gordon Service for more details.
Once a plugin is found, the loader looks up its configuration via the
same key defined in its entry point, e.g. gcp.gpubsub
.
The value of the entry point (e.g. gordon_gcp.gpubsub:EventClient
)
must point to a class. The plugin class is instantiated with its config.
A plugin will not have access to another plugin’s configuration. For
example, the gcp.gpusub
will not have access to the configuration
for gcp.gdns
.
See Gordon’s Plugin System for details on how to write a plugin for Gordon.
Writing a Plugin¶
Todo
Add documentation once interfaces are firmed up
API Reference¶
main¶
Main module to run the Gordon service.
The service expects a gordon.toml
and/or a gordon-user.toml
file for configuration in the current working directory, or in a
provided root directory.
Any configuration defined in gordon-user.toml
overwrites those in
gordon.toml
.
Example:
$ python gordon/main.py
$ python gordon/main.py -c /etc/default/
$ python gordon/main.py --config-root /etc/default/
-
gordon.main.
setup
(config_root='')[source]¶ Service configuration and logging setup.
Configuration defined in
gordon-user.toml
will overwritegordon.toml
.Parameters: config_root (str) – Where configuration should load from, defaults to current working directory. Returns: A dict for Gordon service configuration.
router¶
Core message routing logic for the plugins within Gordon Service.
Messages received on the success channel will be routed to the next
designated plugin phase. For example, a message that has a consume
phase will be routed to the installed enricher provider (or publisher
provider if no enricher provider is installed).
If a message fails its next phase, its phase will be updated to drop
and routed to the event consumer provider for cleanup.
Attention
The GordonRouter
only supports the following two phase
routes:
- consume -> enrich -> publish -> done
- consume -> publish -> done
Future releases may support more configurable phase routings.
-
class
gordon.router.
GordonRouter
(phase_route, success_channel, error_channel, plugins, metrics)[source]¶ Route messages to the appropriate plugin destination.
Attention
error_channel is currently not used in this router, and may be removed entirely from all interface definitions.
Parameters: - phase_route (dict(str, str)) – The route messages should follow.
- success_channel (asyncio.Queue) – A sink for successfully
processed
gordon.interfaces.IEventMessage
s. - error_channel (asyncio.Queue) – A sink for
gordon.interfaces.IEventMessage
s that were not processed due to problems. - plugins (list) – Instantiated message handling plugins.
- metrics (obj) – Implemented
IMetricRelay
interface to emit metrics.
plugins_loader¶
Module for loading plugins distributed via third-party packages.
Plugin discovery is done via entry_points
defined in a package’s
setup.py
, registered under 'gordon.plugins'
. For example:
# setup.py
from setuptools import setup
setup(
name=NAME,
# snip
entry_points={
'gordon.plugins': [
'gcp.gpubsub = gordon_gcp.gpubsub:EventClient',
'gcp.gce.a = gordon_gcp.gce.a:ReferenceSourceClient',
'gcp.gce.b = gordon_gcp.gce.b:ReferenceSourceClient',
'gcp.gdns = gordon_gcp.gdns:DNSProviderClient',
],
},
# snip
)
Plugins are initialized with any config defined in gordon-user.toml
and gordon.toml
. See Configuring the Gordon Service for more details.
Once a plugin is found, the loader looks up its configuration via the
same key defined in its entry point, e.g. gcp.gpubsub
.
The value of the entry point (e.g. gordon_gcp.gpubsub:EventClient
)
must point to a class. The plugin class is instantiated with its config.
A plugin will not have access to another plugin’s configuration. For
example, the gcp.gpusub
will not have access to the configuration
for gcp.gdns
.
See Gordon’s Plugin System for details on how to write a plugin for Gordon.
-
gordon.plugins_loader.
load_plugins
(config, plugin_kwargs)[source]¶ Discover and instantiate plugins.
Parameters: Returns: list of names of plugins, list of instantiated plugin objects, and any errors encountered while loading/instantiating plugins. A tuple of three empty lists is returned if there are no plugins found or activated in gordon config.
Return type: Tuple of 3 lists
interfaces¶
-
interface
gordon.interfaces.
IEventMessage
[source]¶ A discrete unit of work for Gordon to process.
Gordon expects plugins to return or accept objects that implement this interface in order to route them to other plugins, and handle retries or cleanup in case of errors.
-
msg_id
¶ Identifier for the event message instance.
-
phase
¶ Variable phase of the event message.
-
__init__
(msg_id, data, history_log, phase=None)¶ Initialize an EventMessage.
Parameters:
-
append_to_history
(entry, plugin_phase)¶ Append entry to the IEventMessage’s history_log.
Parameters:
-
-
interface
gordon.interfaces.
IRunnable
[source]¶ Extends:
gordon.interfaces.IGenericPlugin
Runnable plugin to produce event messages for Gordon to process.
The plugin also has the ability to send
gordon.interfaces EventMessage
objects to both success and error channels. At least one runnable plugin is required to run Gordon.-
start_phase
¶ Starting phase for event messages.
-
__init__
(config, success_channel, error_channel, metrics, **kwargs)¶ Initialize a runnable plugin.
Parameters: - config (dict) – Plugin-specific configuration.
- success_channel (asyncio.Queue) – A sink for successfully processed IEventMessages.
- error_channel (asyncio.Queue) – A sink for IEventMessages that were not processed due to problems.
- metrics (obj) – Optional obj used to emit metrics.
-
run
()¶ Begin consuming messages using the provided event loop.
-
-
interface
gordon.interfaces.
IMessageHandler
[source]¶ Extends:
gordon.interfaces.IGenericPlugin
Plugin which performs some operation on an event message.
The Gordon core router will use its phase_route to direct messages produced by any runnable plugins the appropriate message handling plugins, identified by their phase attribute. At least one message handling plugin is required to run Gordon.
-
phase
¶ Plugin phase
-
__init__
(config, metrics, **kwargs)¶ Initialize a message handler.
Parameters: - config (dict) – Plugin-specific configuration.
- metrics (obj) – Obj used to emit metrics.
-
handle_message
(event_message)¶ Perform some operation on or triggered by an event message.
Parameters: event_message (IEventMessage) – Message on which to operate.
-
-
interface
gordon.interfaces.
IGenericPlugin
[source]¶ Do not implement this interface directly.
Use
gordon.interfaces.IRunnable
, orgordon.interfaces.IMessageHandler
instead.-
shutdown
()¶ Perform any actions required to gracefully shutdown plugin.
-
-
interface
gordon.interfaces.
IMetricRelay
[source]¶ Manage Gordon metrics.
-
incr
(metric_name, value=1, context=None, **kwargs)¶ Increase a metric by 1 or a given amount.
Parameters:
-
timer
(metric_name, context=None, **kwargs)¶ Get a timer object which implements ITimer.
Parameters:
-
set
(metric_name, value, context=None, **kwargs)¶ Set a metric to a given value.
Parameters:
-
cleanup
(**kwargs)¶ Perform cleanup tasks related to metrics handling.
-
Metrics Implementations¶
ffwd¶
Gordon ships with a simple ffwd metrics implementation, which can be enabled via configuration. This module contains the SimpleFfwdRelay, and all required classes that it uses to send messsages to the ffwd daemon via UDP.
The SimpleFfwdRelay requires no configuration, but can be customized. The defaults that may be overridden are shown below.
[ffwd]
# to identify the service creating metrics
key = 'gordon-service'
# the address of the ffwd daemon (see: UDPClient)
ip = "127.0.0.9"
port = 19000
# a scaling factor for timing (see: FfwdTimer)
time_unit = 1E9
-
class
gordon.metrics.ffwd.
SimpleFfwdRelay
(config)[source]¶ Metrics relay which sends to FFWD immediately.
The relay does no client-side aggregation and metrics are emitted immediately. The relay uses a combination of the key and attributes fields to semantically identify metrics in ffwd.
Parameters: config (dict) – Configuration with optional keys described above. -
incr
(metric_name, value=1, context=None, **kwargs)[source]¶ Increase a metric by 1 or a given amount.
Parameters:
-
-
class
gordon.metrics.ffwd.
FfwdTimer
(metric, udp_client, time_unit=None)[source]¶ Timer which sends UDP messages to FFWD on completion.
Parameters:
-
class
gordon.metrics.ffwd.
UDPClient
(ip=None, port=None, loop=None)[source]¶ Client for sending UDP datagrams.
Parameters:
-
class
gordon.metrics.ffwd.
UDPClientProtocol
(message)[source]¶ Protocol for sending one-off messages via UDP.
Parameters: message (bytes) – Message for ffwd agent. -
connection_made
(transport)[source]¶ Create connection, use to send message and close.
Parameters: transport (asyncio.DatagramTransport) – Transport used for sending.
-
Project Information¶
License and Credits¶
gordon
is licensed under the Apache 2.0 license.
The full license text can be also found in the source code repository.
How to Contribute¶
Every open source project lives from the generous help by contributors that sacrifice their time and gordon
is no different.
This project adheres to the Open Code of Conduct. By participating, you are expected to honor this code. If the core project maintainers/owners feel that this Code of Conduct has been violated, we reserve the right to take appropriate action, including but not limited to: private or public reprimand; temporary or permanent ban from the project; request for public apology.
Communication/Support¶
Feel free to drop by the Spotify FOSS Slack organization in the #gordon channel.
Contributor Guidelines/Requirements¶
Contributors should expect a response within one week of an issue being opened or a pull request being submitted. More time should be allowed around holidays. Feel free to ping your issue or PR if you have not heard a timely response.
Submitting Bugs¶
Before submitting, users/contributors should do the following:
- Basic troubleshooting:
- Make sure you’re on the latest supported version. The problem may be solved already in a later release.
- Try older versions. If you’re on the latest version, try rolling back a few minor versions. This will help maintainers narrow down the issue.
- Try the same for dependency versions - up/downgrading versions.
- Search the project’s issues to make sure it’s not already known, or if there is already an outstanding pull request to fix it.
- If you don’t find a pre-existing issue, check the discussion on Slack. There may be some discussion history, and if not, you can ask for help in figuring out if it’s a bug or not.
What to include in a bug report:
- What version of Python is being used? i.e. 2.7.13, 3.6.2, PyPy 2.0
- What operating system are you on? i.e. Ubuntu 14.04, RHEL 7.4
- What version(s) of the software are you using?
- How can the developers recreate the bug? Steps to reproduce or a simple base case that causes the bug is extremely helpful.
Contributing Patches¶
No contribution is too small. We welcome fixes for typos and grammar bloopers just as much as feature additions and fixes for code bloopers!
- Check the outstanding issues and pull requests first to see if development is not already being done for what you which to change/add/fix.
- If an issue has the
available
label on it, it’s up for grabs for anyone to work on. If you wish to work on it, just comment on the ticket so we can remove theavailable
label. - Do not break backwards compatibility.
- Once any feedback is addressed, please comment on the pull request with a short note, so we know that you’re done.
- Write good commit messages.
Workflow¶
- This project follows the gitflow branching model. Please name your branch accordingly.
- Always make a new branch for your work, no matter how small. Name the branch a short clue to the problem you’re trying to fix or feature you’re adding.
- Ideally, a branch should map to a pull request. It is possible to have multiple pull requests on one branch, but is discouraged for simplicity.
- Do not submit unrelated changes on the same branch/pull request.
- Multiple commits on a branch/pull request is fine, but all should be atomic, and relevant to the goal of the PR. Code changes for a bug fix, plus additional tests (or fixes to tests) and documentation should all be in one commit.
- Pull requests should be rebased off of the
develop
branch. - To finish and merge a release branch, project maintainers should first create a PR to merge the branch into
develop
. Then, they should merge the release branch intomaster
locally and push to master afterwards. - Bugfixes meant for a specific release branch should be merged into that branch through PRs.
Code¶
- See docs on how to setup your environment for development.
- Code should follow the Google Python Style Guide.
- Documentation is not optional.
- Docstrings are required for public API functions, methods, etc. Any additions/changes to the API functions should be noted in their docstrings (i.e. “added in 2.5”)
- If it’s a new feature, or a big change to a current feature, consider adding additional prose documentation, including useful code snippets.
- Tests aren’t optional.
- Any bug fix should have a test case that invokes the bug.
- Any new feature should have test coverage hitting at least $PERCENTAGE.
- Make sure your tests pass on our CI. You will not get any feedback until it’s green, unless you ask for help.
- Write asserts as “expected == actual” to avoid any confusion.
- Add good docstrings for test cases.
Github Labels¶
The super secret decoder ring for the labels applied to issues and pull requests.
Triage Status¶
needs triaging
: a new issue or pull request that needs to be triaged by the goalieno repro
: a filed (closed) bug that can not be reproduced - issue can be reopened and commented upon for more informationwon’t fix
: a filed issue deemed not relevant to the project or otherwise already answered elsewhere (i.e. questions that were answered via linking to documentation or stack overflow, or is about GCP products/something we don’t own)duplicate
: a duplicate issue or pull requestwaiting for author
: issue/PR has questions or requests feedback, and is awaiting the other for a response/update
Development Status¶
To be prefixed with Status:
, e.g. Status: abandoned
.
abandoned
: issue or PR is stale or otherwise abandonedavailable
: bug/feature has been confirmed, and is available for anyone to work on (but won’t be worked on by maintainers)blocked
: issue/PR is blocked (reason should be commented)completed
: issue has been addressed (PR should be linked)wip
: issue is currently being worked onon hold
: issue/PR has development on it, but is currently on hold (reason should be commented)pending
: the issue has been triaged, and is pending prioritization for development by maintainersreview needed
: awaiting a review from project maintainers
Types¶
To be prefixed with Type:
e.g. Type: bug
.
bug
: a bug confirmed via triagefeature
: a feature request/idea/proposalimprovement
: an improvement on existing featuresmaintenance
: a task for required maintenance (e.g. update a dependency for security patches)extension
: issues, feature requests, or PRs that support other services/libraries separate from core
Local Development Environment¶
TODO
Changelog¶
0.0.1.dev6 (2018-06-20)¶
Fixed¶
- Fix routing for handling more than one message at a time.
- Improve warning log messages when loading plugin phase route.
0.0.1.dev4 (2018-06-18)¶
Added¶
- Add logging-based default metrics plugin.
- Emit basic metrics from core router.
- Add a basic graceful shutdown mechanism.
0.0.1.dev3 (2018-06-14)¶
Added¶
- Add
IRunnable
,IMessageHandler
. - Add route configuration requirement.
Changed¶
- Require
IEventMessage
to havephase
andmsg_id
.
Removed¶
- Remove
IEventConsumerClient
,IEnricherClient
,IPublisherClient
.
0.0.1.dev2 (2018-06-13)¶
Added¶
- Add logic to start installed + configured plugins.
- Add initial routing logic for event messages.
- Add interface definitions for a metrics plugin.
- Add FFWD-compatible metrics plugin.
- Enable plugin loader to load metrics plugin.
Fixed¶
- Load config only for active plugins.