Skip to content
This repository was archived by the owner on Sep 12, 2018. It is now read-only.

Commit bba8552

Browse files
author
Andy Goldstein
committed
Extensions via entry points
- auto-loading of modules in the 'docker_registry.extensions' entry point group - preliminary documentation - define and encourage API on top of signals and config
1 parent 2c9bdea commit bba8552

File tree

7 files changed

+167
-29
lines changed

7 files changed

+167
-29
lines changed

docker_registry/extensions/README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# docker-registry extensions
2+
3+
You can further extend the registry behavior by authoring specially crafted Python packages and listening to signals.
4+
5+
## For users
6+
7+
### Word of caution
8+
9+
Installing and running third-party addons usually grant execution privileges and access to sensitive information.
10+
docker-registry extensions are no different, and you need to use caution and to fully understand that said addons once run likely have unlimited access to your configuration, including secrets, certificates, etcetera.
11+
12+
Please give this a think before you run random packages.
13+
14+
### Using an extension
15+
16+
1. Find the extension you are interested in. Usually, a docker container packing that extension is available.
17+
2. Review this extension's configuration settings, edit your configuration file accordingly, and run your registry.
18+
19+
20+
## For developers
21+
22+
23+
### Scaffolding
24+
25+
1. Start a new package (named `foo`)
26+
2. Create whatever module folders you wish (make sure you have an empty `__init__.py` in each folder)
27+
3. Create a setup.py for your package (and then call `python setup.py develop`):
28+
29+
```
30+
setuptools.setup(
31+
entry_points = {
32+
'docker_registry.extensions': [
33+
'[some name] = [your.module]'
34+
]
35+
},
36+
```
37+
38+
`[some name]` is currently ignored but it is a required value.
39+
40+
`[your.module]` is the name of your extension module. It will be imported when the entry point is loaded.
41+
42+
4. Run your registry with DEBUG=true so that gunicorn reloads on file change
43+
44+
### Installing
45+
46+
As soon as your python package is installed on the system, your code will get executed.
47+
The main communication API with the registry is through signals (see below).
48+
49+
As far as your users are concerned, all they have to do should be `pip install my-foo-package` (you may require them to add some specific configuration, including to explicitly enable your extension - see below).
50+
51+
That's it.
52+
53+
54+
### API
55+
56+
This very simple example will print when a tag is created:
57+
58+
```
59+
from docker_registry.lib import signals
60+
61+
62+
def receiver():
63+
print('I am a tag creation receiver')
64+
65+
signals.tag_created.connect(receiver)
66+
67+
```
68+
69+
You can access configuration options through the config API:
70+
71+
```
72+
from docker_registry.lib import config
73+
74+
myconfig = config.load()
75+
```
76+
77+
That will give you the full configuration. We recommend that you place extension configuration settings in the `extensions` portion of the configuration file, e.g.:
78+
79+
```
80+
extensions:
81+
my_cool_extension:
82+
some_key: some_value
83+
```
84+
85+
And you would then reference your configuration options like so:
86+
87+
```
88+
myconfig.extensions.my_cool_extension.some_key
89+
```
90+
91+
For convenience, you may also use the following APIs from core:
92+
93+
```
94+
from docker_registry.core import compat
95+
from docker_registry.core import exceptions
96+
```
97+
98+
(see the code...)
99+
100+
### Versioning and compatibility
101+
102+
Right now there is no versioning strategy (save on core), although we will do our best not to break the API for `signals` and `config`.
103+
104+
Any other docker-registry API should be considered private / unstable, and as a responsible extension author, you should think twice before using said internals...
105+
106+
### Packaging for distribution
107+
108+
The preferred way to package is through pypi.
109+
110+
Your package must depend on `docker-registry-core>=2,<3`
111+
112+
You are strongly encouraged to also provide docker containers for your extension (base it on `FROM registry:latest`), and to give the registry maintainers a ping.

docker_registry/extensions/__init__.py

Whitespace-only changes.

docker_registry/extensions/factory.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pkg_resources
4+
5+
6+
def boot():
7+
for ep in pkg_resources.iter_entry_points('docker_registry.extensions'):
8+
ep.load()
9+
10+
11+
def list():
12+
return list(pkg_resources.iter_entry_points('docker_registry.extensions'))

docker_registry/lib/config.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ class Config(object):
1717
* interpolate from ENV
1818
"""
1919

20-
def __init__(self, config=''):
21-
try:
20+
def __init__(self, config=None):
21+
if config is None:
22+
config = {}
23+
if isinstance(config, compat.basestring):
24+
try:
25+
self._config = yaml.load(config)
26+
except Exception as e:
27+
# Failed yaml loading? Stop here!
28+
raise exceptions.ConfigError(
29+
'Config is not valid yaml (%s): \n%s' % (e, config))
30+
else:
2231
# Config is kept as-is...
2332
self._config = config
24-
# ... save Strings, that are yaml loaded
25-
if isinstance(config, compat.basestring):
26-
self._config = yaml.load(config)
27-
except Exception as e:
28-
# Failed yaml loading? Stop here!
29-
raise exceptions.ConfigError(
30-
'Config is not valid yaml (%s): \n%s' % (e, config))
3133

3234
def __repr__(self):
3335
return repr(self._config)
@@ -77,14 +79,7 @@ def __contains__(self, key):
7779
return key in self._config
7880

7981

80-
_config = None
81-
82-
83-
def load():
84-
global _config
85-
if _config is not None:
86-
return _config
87-
82+
def _init():
8883
flavor = os.environ.get('SETTINGS_FLAVOR', 'dev')
8984
config_path = os.environ.get('DOCKER_REGISTRY_CONFIG', 'config.yml')
9085

@@ -97,25 +92,36 @@ def load():
9792
raise exceptions.FileNotFoundError(
9893
'Heads-up! File is missing: %s' % config_path)
9994

100-
_config = Config(f.read())
95+
conf = Config(f.read())
10196
if flavor:
102-
_config = _config[flavor]
103-
_config.flavor = flavor
97+
conf = conf[flavor]
98+
conf.flavor = flavor
10499

105-
if _config.privileged_key:
100+
if conf.privileged_key:
106101
try:
107-
f = open(_config.privileged_key)
102+
f = open(conf.privileged_key)
108103
except Exception:
109104
raise exceptions.FileNotFoundError(
110-
'Heads-up! File is missing: %s' % _config.privileged_key)
105+
'Heads-up! File is missing: %s' % conf.privileged_key)
111106

112107
try:
113-
_config.privileged_key = rsa.PublicKey.load_pkcs1(f.read())
108+
conf.privileged_key = rsa.PublicKey.load_pkcs1(f.read())
114109
except Exception:
115110
raise exceptions.ConfigError(
116-
'Key at %s is not a valid RSA key' % _config.privileged_key)
111+
'Key at %s is not a valid RSA key' % conf.privileged_key)
112+
113+
if conf.index_endpoint:
114+
conf.index_endpoint = conf.index_endpoint.strip('/')
115+
116+
return conf
117+
118+
_config = None
119+
120+
121+
def load():
122+
global _config
117123

118-
if _config.index_endpoint:
119-
_config.index_endpoint = _config.index_endpoint.strip('/')
124+
if not _config:
125+
_config = _init()
120126

121127
return _config

docker_registry/wsgi.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
except Exception as e:
1616
raise(Exception('Failed to init new relic agent %s' % e))
1717

18+
from .extensions import factory
19+
factory.boot()
1820
from .run import app
1921

2022
if __name__ == '__main__':

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
'docker_registry.server',
5151
'docker_registry.lib',
5252
'docker_registry.storage',
53-
'docker_registry.lib.index']
53+
'docker_registry.lib.index',
54+
'docker_registry.extensions']
5455

5556
namespaces = ['docker_registry', 'docker_registry.drivers']
5657

tests/test_config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ def setUp(self):
3232

3333
self.c = config.Config(open(p, 'rb').read())
3434

35-
def test__init__(self):
35+
def test__init__parse_error(self):
3636
self.assertRaises(config.exceptions.ConfigError, config.Config, '\1')
3737

38+
def test__init__no_arg(self):
39+
self.c = config.Config()
40+
assert self.c['whatevertheflush'] is None
41+
assert self.c.whatevertheflush is None
42+
3843
@mock.patch('__builtin__.repr')
3944
def test__repr(self, r):
4045
self.c.__repr__()

0 commit comments

Comments
 (0)