2
0
mirror of https://github.com/cmehay/pyentrypoint synced 2024-11-10 13:10:37 +00:00

Release v0.1.9 beta

This commit is contained in:
Christophe Mehay 2016-02-21 22:11:37 +01:00
parent 36dc9ba11c
commit 7735943986
19 changed files with 391 additions and 187 deletions

View File

@ -7,6 +7,7 @@
- id: check-yaml
- id: end-of-file-fixer
- id: flake8
exclude: __init__.py
- id: name-tests-test
- id: autopep8-wrapper
- id: requirements-txt-fixer

View File

@ -7,11 +7,10 @@ RUN pip install pytest twiggy six pyyaml jinja2
ENV PYTHONPATH /opt/pyentrypoint/
ADD pyentrypoint /opt/pyentrypoint/
ADD tests /opt/pyentrypoint/tests
ADD tests/entrypoint-config.yml /opt/pyentrypoint/
ADD tests /opt/
ADD tests/test_template.yml.tpl /tmp/test_template.yml
WORKDIR /opt/pyentrypoint/
WORKDIR /opt/
CMD ["py.test", "-s", "."]

View File

@ -7,11 +7,11 @@ RUN pip3 install pytest twiggy six pyyaml jinja2
ENV PYTHONPATH /opt/pyentrypoint/
ADD pyentrypoint /opt/pyentrypoint/
ADD tests /opt/pyentrypoint/tests
ADD tests/entrypoint-config.yml /opt/pyentrypoint/
ADD tests /opt/
ADD tests/test_template.yml.tpl /tmp/test_template.yml
WORKDIR /opt/pyentrypoint/
WORKDIR /opt/
CMD ["py.test", "-s", "."]

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include README.md

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
.PHONY: build test
build:
@docker-compose build
test: build
@docker-compose up testpython2 testpython3

279
README.md
View File

@ -1,157 +1,196 @@
# py_docker_links
# pyentrypoint
py_docker_links is a kiss python module which helps to list
linked containers inner containers.
__pyentrypoint__ is a tool written in `Python` to manager Docker containers `ENTRYPOINT`.
You can use it with `ENTRYPOINT` script to generate configuration.
This tool avoids writing shell scripts to:
- Handle commands and sub commands
- Identify linked containers
- Generate configuration using `jinja2` templates
- Run commands before starting service
## Usages
We have some containers described in `docker-compose.yml`
### Install in container
All you need to do is to setup a `yaml` file called `entrypoint-config.yml` and to install __pyentrypoint__ in your `Dockerfile` using pip.
```dockerfile
FROM debian
# Installing git for example
RUN apt-get update && apt-get install git -y
# Install pyentrypoint
RUN pip install pyentrypoint
# Copy config file in the current WORKDIR
COPY entrypoint-config.yml .
# Set ENTRYPOINT
ENTRYPOINT ['pyentrypoint']
# git will be the default command
CMD ['git']
```
### Setup entrypoint
This is an example of `entrypoint-config.yml` file.
```yaml
# Here some dummies containers
test1:
image: busybox
command: sleep 30
expose:
- 800
- 8001/udp
environment:
FOO: bar
# Entrypoint configuration example
test2:
image: busybox
command: sleep 30
expose:
- 800/udp
- 8001
# This entry should reflect CMD in Dockerfile
command: git
test3:
image: busybox
command: sleep 30
environment:
FOO: bar
# This is a list with some subcommands to handle
# when CMD is not `git` here.
# By default, all args started with hyphen are handled.
subcommands:
- "-*"
- clone
- init
- ls-files
# etc...
# User and group to run the cmd.
# Can be name or uid/gid.
# Affect only command handled.
# Dockerfile USER value by default.
user: 1000
group: 1000
# Here our container that embed docker_links.py linked
# with dummies containers
dockerlinks:
build: .
dockerfile: Dockerfile.py3
command: python docker_links.py
# These files should exist (ADD or COPY)
# and should be jinja templated.
config_files:
- /etc/gitconfig
- .ssh/config
- .ssh/id_rsa
# These environment variables will be wiped before
# exec command to keep them secret
# CAUTION: if the container is linked to another one,
# theses variables will passed to it anyway
secret_env:
- SSHKEY
# Links are handled here
# Port, name, protocol or env variable can be used to identify the links
# Raise an error if the link could not be identified
links:
- test1
- test2
- test3
'ssh':
port: 22
name: 'ssh*'
protocol: tcp
# env can be list, dictionary or string
env:
FOO: bar
# Single doesn't allow multiple links for this ID
# false by default
single: true
# Set to false to get optional link
# true by default
required: true
# Commands to run before applying configuration
pre_conf_commands:
- echo something > to_this_file
# commands to run after applying configuration
post_conf_commands:
- echo "something else" > to_this_another_file
# Cleanup environment from variables created by linked containers
# before running command (True by default)
clean_env: True
# Enable debug to debug
debug: true
```
Start them
### Config templates
```shell
$ docker-compose build && docker-compose up dockerlinks
You can generate configuration for your service with jinga2 template.
Here an example for an hypothetical ssh config file:
```jinga
host server:
hostname {{links.ssh.ip}}
port {{links.ssh.port}}
```
We should get formated json with informations about linked containers.
Templates with be replaced with ip address and port of the identified link. All links can be accessed from `links.all`, this is a tuple of links you can iterate on it.
```json
{
"172.17.0.2": {
"environment": {
"FOO": "bar",
"affinity:container": "=a5601d5d225a3e57ea295c7646468067dd1859d4b2ee4574b5bf5542ed372e59"
},
"names": [
"d778f6ef9371",
"pythondockertools_test3_1",
"test3",
"test3_1"
],
"ports": {}
},
"172.17.0.3": {
"environment": {
"affinity:container": "=78393f27c629fc426af5837a11d30720c8af7a5e029eb173b394f207e7e4701c"
},
"names": [
"5fc12cf7b49e",
"pythondockertools_test2_1",
"test2",
"test2_1"
],
"ports": {
"800": {
"protocol": "tcp"
},
"8001": {
"protocol": "tcp"
}
}
},
"172.17.0.4": {
"environment": {
"FOO": "bar",
"affinity:container": "=6a31a66a1aafcd607763dcd916b81b4385a3baf4354c044345255c3eb0bce925"
},
"names": [
"d32fc2303721",
"pythondockertools_test1_1",
"test1",
"test1_1"
],
"ports": {
"800": {
"protocol": "tcp"
},
"8001": {
"protocol": "udp"
}
}
}
}
```jinga
{% for link in links.all %}
host {{link.names[0]}}
hostname {{link.ip}}
port {{links.port}}
{% endfor %}
```
#### Using as module
```python
from docker_links import DockerLinks
links = DockerLinks()
print(links.links())
```
You'll get a dictionary with all linked containers
```python
{'172.17.0.2': {'environment': {'affinity:container': '=6a31a66a1aafcd607763dcd916b81b4385a3baf4354c044345255c3eb0bce925', 'FOO': 'bar'}, 'ports': {'800': {'protocol': 'tcp'}, '8001': {'protocol': 'udp'}}, 'names': ['d32fc2303721', 'pythondockertools_test1_1', 'test1', 'test1_1']}, '172.17.0.3': {'environment': {'affinity:container': '=78393f27c629fc426af5837a11d30720c8af7a5e029eb173b394f207e7e4701c'}, 'ports': {'800': {'protocol': 'tcp'}, '8001': {'protocol': 'tcp'}}, 'names': ['5fc12cf7b49e', 'pythondockertools_test2_1', 'test2', 'test2_1']}, '172.17.0.5': {'environment': {'affinity:container': '=a5601d5d225a3e57ea295c7646468067dd1859d4b2ee4574b5bf5542ed372e59', 'FOO': 'bar'}, 'ports': {}, 'names': ['d778f6ef9371', 'pythondockertools_test3_1', 'test3', 'test3_1']}}
If you change the option `single` to `false` in the `entrypoint-config.yml`, the identified link `ssh` will become a tuple of links. You must iterate on it in the `jinja` template.
```jinga
{% for link in links.ssh %}
host {{link.names[0]}}
hostname {{link.ip}}
port {{links.port}}
{% endfor %}
```
---
You call also get a pretty print json formating
### Accessible object
```python
from docker_links import DockerLinks
You have 4 available objects in your templates.
links = DockerLinks()
- `config`
- `links`
- `containers`
- `environ`
print(links.to_json())
```
#### config
or filter links
`Config` reflect the config file. You can retrieve any setup in this object.
```python
from docker_links import DockerLinks
(see `config.py`)
links = DockerLinks()
#### links
print(links.links('test1', 'test2')) # It also works with container uid
```
`Links` handles `Link` objects. You can identify links using globing patterns in the configuration file.
`link` is related to one physical link (one ip and one port).
`link` handles the following attributes:
- `ip`
- link ip
- `port`
- link port (integer)
- `environ`
- related container environment
- `protocol`
- link protocol (`tcp` or `udp`)
- `uri`
- link uri (example: `tcp://10.0.0.3:80`)
- `names`
- tuple of related container names
#### containers
`containers` handles a tuple of `container` object.
`container` handles the following attributes:
- `ip`
- container ip
- `environ`
- container environment
- `names`
- List of containers names
- `links`
- Tuple of `link` object related to this container
#### environ
`environ` is the environment of the container (os.environ).
### Running Tests
To run tests, ensure that docker-compose is installed and run
To run tests, ensure that `docker-compose` and `make` are installed and run
```shell
docker-compose build && docker-compose up testpython2 testpython3
$ make test
```

View File

@ -0,0 +1,7 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from .docker_links import DockerLinks
from .entrypoint import Entrypoint
__version__ = '0.1.1'

10
pyentrypoint/__main__.py Normal file
View File

@ -0,0 +1,10 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from sys import argv
from .entrypoint import main as m
def main():
m(argv)

View File

@ -1,4 +1,7 @@
"Command object"
from __future__ import absolute_import
from __future__ import unicode_literals
import fnmatch
import os
@ -35,8 +38,8 @@ class Command(object):
def run(self):
if os.getuid() is 0:
os.setuid(self.config.user)
os.setgid(self.config.group)
os.setuid(self.config.user)
if self.config.clean_env:
self._clean_links_env()
for item in self.config.secret_env:
@ -44,6 +47,5 @@ class Command(object):
del(self.env[item])
if not self.args or \
fnmatch.filter(self.config.subcommands, self.args[0]):
args = self.args if self.args else [self.command]
os.execvpe(self.command, args, os.environ)
os.execvpe(args[0], args, os.environ)
self.args.insert(0, self.command)
os.execvpe(self.args[0], self.args, os.environ)

View File

@ -1,25 +1,35 @@
"""
Configuration
Configuration object
"""
from __future__ import absolute_import
from __future__ import unicode_literals
import os
from grp import getgrnam
from io import open
from pwd import getpwnam
from command import Command
from docker_links import DockerLinks
from links import Links
from six import string_types
from yaml import load
from yaml import Loader
from .command import Command
from .docker_links import DockerLinks
from .links import Links
__all__ = ['Config']
class Config(object):
"""Get entrypoint config"""
"""
Get entrypoint config
Parse entrypoint-config.yml
Config file should always be in WORKDIR and named entrypoint-config.yml
"""
# Config file should always be in WORKDIR and named
# entrypoint-config.yml
_config_file = 'entrypoint-config.yml'
def _return_item_lst(self, item):
@ -36,12 +46,8 @@ class Config(object):
self._links = None
if not os.path.isfile(self._config_file):
return
try:
with open(self._config_file) as f:
self._config = load(stream=f, Loader=Loader)
except Exception as err:
# TODO: logger
print(err)
self._args = args
@property
@ -110,14 +116,12 @@ class Config(object):
@property
def pre_conf_commands(self):
"""Return list of preconf commands"""
if 'pre_conf_commands' in self._config:
return self._return_item_lst(self._config['pre_conf_command'])
return self._return_item_lst('pre_conf_commands')
@property
def post_conf_commands(self):
"""Return list of postconf commands"""
if 'post_conf_commands' in self._config:
return self._return_item_lst(self._config['post_conf_command'])
return self._return_item_lst('post_conf_commands')
@property
def clean_env(self):

View File

@ -1,6 +1,8 @@
"""
Container object handle a single container link
"""
from __future__ import absolute_import
from __future__ import unicode_literals
class Container(object):

View File

@ -3,12 +3,17 @@
DockerLinks a kiss class which help to get links info in a docker
container.
"""
from __future__ import absolute_import
from __future__ import unicode_literals
import json
import os
import re
from container import Container
from links import Links
from .container import Container
from .links import Links
__all__ = ['DockerLinks']
class DockerLinks(object):

View File

@ -2,20 +2,26 @@
"""
Smart docker-entrypoint
"""
from __future__ import absolute_import
from __future__ import unicode_literals
import os
from subprocess import PIPE
from subprocess import Popen
from sys import argv
from sys import stdout
from command import Command
from config import Config
from docker_links import DockerLinks
from jinja2 import Environment
from jinja2 import FileSystemLoader
from twiggy import levels
from twiggy import log
from twiggy import quickSetup
from .config import Config
from .docker_links import DockerLinks
__all__ = ['Entrypoint', 'main']
class Entrypoint(object):
@ -28,7 +34,7 @@ class Entrypoint(object):
def __init__(self, args=[]):
self._set_logguer()
try:
self.config = Config()
self.config = Config(args)
except Exception as err:
self.log.error(err)
if self.config.debug:
@ -51,25 +57,42 @@ class Entrypoint(object):
proc = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
out, err = proc.communicate()
self.log.info(out)
self.log.warning(err)
def dispout(output, cb):
enc = stdout.encoding or 'UTF-8'
output = output.decode(enc).split('\n')
for line in output:
cb(line)
if out:
dispout(out, self.log.info)
if err:
dispout(err, self.log.warning)
if proc.returncode:
raise Exception('Command exit code: {}'.format(proc.returncode))
def run_pre_conf_cmds(self):
for cmd in self.config.pre_conf_commands:
self.run_conf_cmd(cmd)
def run_post_conf_cmds(self):
for cmd in self.config.post_conf_commands:
self.run_conf_cmd(cmd)
def launch(self):
self.args.pop(0)
command = Command(self.config, self.args)
command.run()
self.config.command.run()
def main(argv):
argv.pop(0)
entry = Entrypoint(args=argv)
try:
entry.run_pre_conf_cmds()
entry.apply_conf()
entry.run_post_conf_cmds()
entry.launch()
except Exception as e:
entry.log.error(str(e))
if __name__ == '__main__':
entry = Entrypoint(argv)
try:
for cmd in entry.config.pre_conf_commands:
entry.run_conf_cmd(cmd)
entry.apply_conf()
for cmd in entry.config.post_conf_commands:
entry.run_conf_cmd(cmd)
entry.launch()
except Exception as e:
print(e)
main(argv)

View File

@ -1,7 +0,0 @@
"""
Custom exceptions
"""
class BadLink(Exception):
pass

View File

@ -1,10 +1,15 @@
"""
Link handle a single link to another container, determined by his port
"""
from __future__ import absolute_import
from __future__ import unicode_literals
import fnmatch
from six import viewitems
__all__ = ['Link', 'Links']
class Link(object):
@ -12,7 +17,7 @@ class Link(object):
def __init__(self, ip, env, port, protocol, names):
self.ip = ip
self.env = env
self.environ = env
self.port = int(port)
self.protocol = protocol
self.uri = '{protocol}://{ip}:{port}'.format(
@ -37,10 +42,10 @@ class Link(object):
def _filter_env(self, env):
"return true if env match"
if isinstance(env, dict):
return viewitems(env) <= viewitems(self.env)
return viewitems(env) <= viewitems(self.environ)
if isinstance(env, list):
return bool([key for key in env if key in self.env])
return str(env) in self.env
return bool([key for key in env if key in self.environ])
return str(env) in self.environ
class Links(object):

56
setup.py Normal file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import find_packages
from setuptools import setup
# Thanks Sam and Max
setup(
name='pyentrypoint',
version='0.1.9',
packages=find_packages(),
author="Christophe Mehay",
author_email="cmehay@nospam.student.42.fr",
description="pyentrypoint manages entrypoints in Docker containers.",
long_description=open('README.md').read(),
install_requires=['Jinja2>=2.8',
'PyYAML>=3.11',
'Twiggy>=0.4.7',
'argparse>=1.4.0',
'six>=1.10.0'],
include_package_data=True,
url='http://github.com/cmehay/pyentrypoint',
classifiers=[
"Programming Language :: Python",
"Development Status :: 1 - Planning",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Topic :: System :: Installation/Setup",
],
entry_points={
'console_scripts': [
'pyentrypoint = pyentrypoint.__main__:main',
],
},
license="WTFPL",
)

View File

@ -23,3 +23,14 @@ links:
env:
FOO: bar
required: true
pre_conf_commands:
- echo TEST > /tmp/OK
- echo "INFO IS DISPLAYED"
- echo "WARNING IS DISPLAYED\nON TWO LINES" 1>&2
post_conf_commands:
- echo TEST2 > /tmp/OKOK
- echo TEST3 > /tmp/OKOKOK
- echo "INFO IS DISPLAYED\nON TWO LINES"
- echo "WARNING IS DISPLAYED" 1>&2

View File

@ -1,11 +1,16 @@
# Tests using pytest
import fnmatch
from __future__ import absolute_import
from __future__ import unicode_literals
import fnmatch
from multiprocessing import Process
from docker_links import DockerLinks
from entrypoint import Entrypoint
from yaml import load
from yaml import Loader
from pyentrypoint import DockerLinks
from pyentrypoint import Entrypoint
LINKS = [
'test1',
'test2',
@ -114,3 +119,36 @@ def test_templates():
# test names
for test_name in test_names:
assert test_name in test['All names']
def test_conf_commands():
entry = Entrypoint()
for cmd in entry.config.pre_conf_commands:
entry.run_conf_cmd(cmd)
for cmd in entry.config.post_conf_commands:
entry.run_conf_cmd(cmd)
with open('/tmp/OK') as f:
assert f.readline().startswith('TEST')
with open('/tmp/OKOK') as f:
assert f.readline().startswith('TEST2')
with open('/tmp/OKOKOK') as f:
assert f.readline().startswith('TEST3')
def test_command():
run = [
(Process(target=Entrypoint(['OK']).launch), 'OK\n'),
(Process(target=Entrypoint(['echo', 'mdr']).launch), 'mdr\n'),
(Process(target=Entrypoint(['OK', 'mdr']).launch), 'OK mdr\n'),
]
# capsys.readouterr()
for proc, test in run:
proc.start()
proc.join()
# out, _ = capsys.readouterr()
# assert out == test

View File

@ -19,15 +19,15 @@ All links 2:
All environ:
{% for link in links.all %}
{% for key in link.env %}
{{key}}: {{link.env[key]}}
{% for key in link.environ %}
{{key}}: {{link.environ[key]}}
{% endfor %}
{% endfor %}
All links 2 environ:
{% for link in links.test2 %}
{% for key in link.env %}
{{key}}: {{link.env[key]}}
{% for key in link.environ %}
{{key}}: {{link.environ[key]}}
{% endfor %}
{% endfor %}