You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

155 lines
5.0 KiB

from __future__ import print_function
from io import StringIO
import jinja2
import json
import logging
import os
import sys
import sh
import tempfile
from enough.version import __version__
from enough.common.retry import retry, RetryException
log = logging.getLogger(__name__)
class Docker(object):
def __init__(self, **kwargs):
self.root = kwargs.get('root', os.path.join(os.path.dirname(__file__), '..'))
self.name = kwargs.get('name')
self.port = kwargs.get('port', '8000')
self.retry = kwargs.get('retry', 9)
self.domain = kwargs.get('domain', 'enough.community')
self.bake_docker(kwargs.get('docker'))
self.bake_docker_compose(kwargs.get('docker_compose'))
def bake_docker(self, docker):
cmd = docker or 'docker'
self.docker = sh.Command(cmd).bake(
_truncate_exc=False,
_tee=True,
_tty_out=False,
_out=lambda x: log.info(x.strip()),
_err=lambda x: log.info(x.strip()),
)
def bake_docker_compose(self, docker_compose):
content = self.get_compose_content()
if log.getEffectiveLevel() <= logging.DEBUG:
# do not use log.debug() because the output is unreadable on a single line
# and multiline logging is bad practice
print(content)
compose = tempfile.NamedTemporaryFile()
compose.write(content.encode('utf-8'))
compose.flush()
cmd = docker_compose or 'docker-compose'
self.docker_compose = sh.Command(cmd).bake(
'--project-name', 'enough',
'--file', compose.name,
_truncate_exc=False,
_tee=True,
_tty_out=False,
_out=lambda x: log.info(x.strip()),
_err=lambda x: log.info(x.strip()),
)
self.compose_file = compose # so that it is kept until self is deleted
def create_network(self, name):
existing = self.docker.network.ls('--format={{ .Name }}',
f'--filter=name={name}').stdout
if existing != b'':
return False
self.docker.network.create(name)
return True
def get_compose_content(self):
f = os.path.join(self.root, 'common/data/docker-compose.yml')
return self.replace_content(open(f).read())
def replace_content(self, content):
return jinja2.Template(content).render(this=self)
def get_ports(self):
return str(self.port) + ':8000'
def create_image(self):
dockerfile = os.path.join(self.root, 'common/data/base.dockerfile')
return self._create_image('base', '-f', dockerfile, '.')
def _create_image(self, suffix, *args):
name = self.get_image_name_with_version(suffix)
build_args = ['--quiet', '--tag', name]
self.docker.build(build_args + list(args))
self.docker.tag(name, self.get_image_name(suffix))
return name
def get_image_name(self, suffix):
if suffix:
return self.name + '_' + suffix
else:
return self.name
def get_image_name_with_version(self, suffix):
return self.get_image_name(suffix) + ':' + str(__version__)
def down(self):
self.docker_compose('down')
def up_wait_for_services(self):
self.up()
try:
self.wait_for_services()
except RetryException:
self.print_logs()
raise
def create_or_update(self):
if self.inspect('placeholder'):
return False
self.create_image()
self.up_wait_for_services()
return True
def get_ip(self):
return self.inspect('{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')[0]
def get_public_port(self, private_port):
bindings = self.inspect('{{ json .HostConfig.PortBindings }}')
if bindings:
bindings = json.loads(bindings[0])
return bindings[f'{private_port}/tcp'][0]['HostPort']
else:
return None
def up(self):
self.docker_compose('up', '-d')
def inspect(self, format):
ids = self.docker_compose('ps', '-q', _iter=True)
results = []
for id in ids:
id = id.strip()
r = self.docker('inspect', f'--format={format}', id)
results.append(r.stdout.decode('utf-8').strip())
return results
def wait_for_services(self):
@retry(AssertionError, tries=self.retry)
def wait():
results = self.inspect('{{ .State.Health.Status }}')
assert all([x.strip() == 'healthy' for x in results]), str(results)
log.info("enough service " + self.name + " is healthy")
wait()
def get_logs(self):
result = StringIO()
print(f"docker compose logs {self.name}\n", file=result)
self.docker_compose('logs', _out=result)
print(str(self.inspect('{{ json .State.Health }}')))
result.write(str(self.inspect('{{ json .State.Health }}')))
return result.getvalue()
def print_logs(self):
print(self.get_logs(), file=sys.stderr)