Skip to content

Adding a custom Source

The sources built-in to Sovereign are quite limited, and not all data can be retrieved from a file or from a simple HTTP API.

Sovereign was built to be extended for this reason. Sources can be added by writing a Python module and adding it to a list of entry points that will be loaded when Sovereign starts.

Tutorial: A DNS Service-Discovery Source

In this tutorial we'll go through the steps required to create a Source that will resolve SRV records using DNS and create Instances based on the results.

Create a Python module

First, create an empty Python module.

For this example the following folder structure will be used:

├───my_custom_source
│   ├───__init__.py   # <- This file is left empty
│   └───service_discovery.py
└───setup.py

Add a Source to the module

Sovereign provides a Source class which has some methods that must be implemented.

# my_custom_source/service_discovery.py
from typing import List
from dns.resolver import Resolver
from sovereign.sources.lib import Source


class ServiceDiscovery(Source):
    """
    Finds clusters using SRV records
    """

    # If the init needs to be overwritten, it should start as follows
    def __init__(self, config, scope='default'):
        super(ServiceDiscovery, self).__init__(config, scope)
        # -- Start of custom init
        if scope not in ('clusters', 'endpoints'):
            raise ValueError('This source is only supported for clusters/endpoints')

        self.resolver = Resolver()
        configured_resolvers = config.get('resolvers', [])
        if configured_resolvers:
            self.resolver.nameservers = configured_resolvers
        else:
            self.logger.msg('Using resolvers from /etc/resolv.conf')

    def get(self) -> List[dict]:
       for srv in self.config.get('srv_records', []):
           query = self.resolver.resolve(srv, rdtype='SRV')
           instance = {
               'name': srv,
               'hosts': []
           }
           for answer in query.response.answer:
               *_, priority, weight, port, target = answer.to_text().split()
               instance['hosts'].append({
                   'address': target,
                   'port': port,
                   'weight': weight,
                   'priority': priority,
               })
           yield instance

Write a setuptools script

The following script adds your Python module to the list of sources, which Sovereign checks at runtime:

from setuptools import setup, find_packages

setup(
    name='my_custom_source',
    packages=find_packages(),
    entry_points={
        "sovereign.sources": [
            "service_discovery = my_custom_source.service_discovery:ServiceDiscovery",
        ]
    }
)

This will install the above Python module into an entry point named sovereign.sources,
with a name of service_discovery

Install the Python module in the same place that you installed Sovereign

You'll need to run the above setup script wherever you've installed Sovereign, using pip install sovereign or similar.

Simply run python setup.py install and you should see output similar to the following:

$ python setup.py install
running install
running bdist_egg
running egg_info
creating my_custom_source.egg-info
writing my_custom_source.egg-info/PKG-INFO
writing dependency_links to my_custom_source.egg-info/dependency_links.txt
writing entry points to my_custom_source.egg-info/entry_points.txt
writing top-level names to my_custom_source.egg-info/top_level.txt
writing manifest file 'my_custom_source.egg-info/SOURCES.txt'
reading manifest file 'my_custom_source.egg-info/SOURCES.txt'
writing manifest file 'my_custom_source.egg-info/SOURCES.txt'
installing library code to build/bdist.macosx-10.9-x86_64/egg
running install_lib
running build_py
creating build
creating build/lib
creating build/lib/my_custom_source
copying my_custom_source/__init__.py -> build/lib/my_custom_source
copying my_custom_source/service_discovery.py -> build/lib/my_custom_source
creating build/bdist.macosx-10.9-x86_64
creating build/bdist.macosx-10.9-x86_64/egg
creating build/bdist.macosx-10.9-x86_64/egg/my_custom_source
copying build/lib/my_custom_source/__init__.py -> build/bdist.macosx-10.9-x86_64/egg/my_custom_source
copying build/lib/my_custom_source/service_discovery.py -> build/bdist.macosx-10.9-x86_64/egg/my_custom_source
byte-compiling build/bdist.macosx-10.9-x86_64/egg/my_custom_source/__init__.py to __init__.cpython-38.pyc
byte-compiling build/bdist.macosx-10.9-x86_64/egg/my_custom_source/service_discovery.py to service_discovery.cpython-38.pyc
creating build/bdist.macosx-10.9-x86_64/egg/EGG-INFO
copying my_custom_source.egg-info/PKG-INFO -> build/bdist.macosx-10.9-x86_64/egg/EGG-INFO
copying my_custom_source.egg-info/SOURCES.txt -> build/bdist.macosx-10.9-x86_64/egg/EGG-INFO
copying my_custom_source.egg-info/dependency_links.txt -> build/bdist.macosx-10.9-x86_64/egg/EGG-INFO
copying my_custom_source.egg-info/entry_points.txt -> build/bdist.macosx-10.9-x86_64/egg/EGG-INFO
copying my_custom_source.egg-info/top_level.txt -> build/bdist.macosx-10.9-x86_64/egg/EGG-INFO
zip_safe flag not set; analyzing archive contents...
creating dist
creating 'dist/my_custom_source-0.0.0-py3.8.egg' and adding 'build/bdist.macosx-10.9-x86_64/egg' to it
removing 'build/bdist.macosx-10.9-x86_64/egg' (and everything under it)
Processing my_custom_source-0.0.0-py3.8.egg
Copying my_custom_source-0.0.0-py3.8.egg to ....../lib/python3.8/site-packages
Adding my-custom-source 0.0.0 to easy-install.pth file

Installed ....../lib/python3.8/site-packages/my_custom_source-0.0.0-py3.8.egg
Processing dependencies for my-custom-source==0.0.0

Configuring Sovereign to use the Source

Similar to how you would use a file/inline source, add it to the list of sources with the type, scope, and config. Example:

sources:
  - type: service_discovery
    scope: clusters
    config:
      srv_records:
        # - '_service._proto.domain.tld.'
        - '_imaps._tcp.gmail.com.'  # Real example

The above example config should result in something like the following being added to the list of Instances:

{
  "name": "_imaps._tcp.gmail.com.", 
  "hosts": [{
    "address": "imap.gmail.com.",
    "port": "993", 
    "weight": "0", 
    "priority": "5"
  }]
}

This data could then be used in a template. Let's say for example we have a clusters template that looked like so:

resources:
{% for cluster in clusters %}
- name: {{ cluster['name'] }}
  connect_timeout: 0.25s
  type: STRICT_DNS
  load_assignment:
    cluster_name: {{ cluster['name'] }}
    endpoints:
      {% for host in cluster['hosts'] %}
      - priority: {{ host['priority'] }}
        load_balancing_weight: {{ host['weight'] }}
        lb_endpoints:
          - endpoint:
              address:
                socket_address:
                  address: {{ host['address'] }}
                  port_value: {{ host['port'] }}
      {% endfor %}
{% endfor %}

TODO: verification / run the server and look at the clusters

Recap

  • We created a Python module, containing an object that inherits from Source from the Sovereign library
  • We added code that does a DNS lookup on SRV records using a 3rd party library and parses the output into instances
  • We made a setup script, and installed it to the same machine which has Sovereign installed
  • We added the service_discovery source to the list of sources, with an example list of SRV records to lookup
  • We verified ... TODO