2017-05-09 08:37:19 +00:00
|
|
|
import os
|
2017-05-19 07:07:39 +00:00
|
|
|
import sys
|
2017-05-09 08:37:19 +00:00
|
|
|
import subprocess
|
2017-05-25 22:37:33 +00:00
|
|
|
from textwrap import dedent
|
2017-05-09 08:37:19 +00:00
|
|
|
|
2017-05-09 20:39:03 +00:00
|
|
|
import docker
|
2017-05-25 22:09:26 +00:00
|
|
|
from docker.utils import kwargs_from_env
|
2017-05-09 20:39:03 +00:00
|
|
|
|
2017-05-24 21:11:37 +00:00
|
|
|
from traitlets import Unicode, Dict, Bool
|
2017-05-09 08:37:19 +00:00
|
|
|
from traitlets.config import LoggingConfigurable
|
|
|
|
|
2017-05-16 01:54:51 +00:00
|
|
|
import logging
|
|
|
|
from pythonjsonlogger import jsonlogger
|
|
|
|
|
|
|
|
from .utils import execute_cmd
|
|
|
|
|
2017-05-24 16:49:23 +00:00
|
|
|
here = os.path.abspath(os.path.dirname(__file__))
|
2017-05-09 08:37:19 +00:00
|
|
|
|
|
|
|
class BuildPack(LoggingConfigurable):
|
2017-05-16 01:54:51 +00:00
|
|
|
name = Unicode()
|
2017-05-24 21:11:37 +00:00
|
|
|
capture = Bool(False, help="Capture output for logging")
|
2017-05-16 01:54:51 +00:00
|
|
|
|
2017-05-09 08:37:19 +00:00
|
|
|
def detect(self, workdir):
|
|
|
|
"""
|
|
|
|
Return True if app in workdir can be built with this buildpack
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
2017-05-22 03:39:36 +00:00
|
|
|
def build(self, workdir, ref, output_image_spec):
|
2017-05-09 08:37:19 +00:00
|
|
|
"""
|
|
|
|
Run a command that will take workdir and produce an image ready to be pushed
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2017-05-09 20:39:03 +00:00
|
|
|
class DockerBuildPack(BuildPack):
|
2017-05-16 01:54:51 +00:00
|
|
|
name = Unicode('Dockerfile')
|
2017-05-09 20:39:03 +00:00
|
|
|
def detect(self, workdir):
|
|
|
|
return os.path.exists(os.path.join(workdir, 'Dockerfile'))
|
|
|
|
|
2017-05-22 03:39:36 +00:00
|
|
|
def build(self, workdir, ref, output_image_spec):
|
2017-05-25 22:09:26 +00:00
|
|
|
client = docker.APIClient(version='auto', **kwargs_from_env())
|
2017-05-09 20:39:03 +00:00
|
|
|
for progress in client.build(
|
|
|
|
path=workdir,
|
|
|
|
tag=output_image_spec,
|
|
|
|
decode=True
|
|
|
|
):
|
2017-05-16 01:54:51 +00:00
|
|
|
if 'stream' in progress:
|
2017-05-25 22:10:08 +00:00
|
|
|
if self.capture:
|
|
|
|
self.log.info(progress['stream'], extra=dict(phase='building'))
|
|
|
|
else:
|
|
|
|
sys.stdout.write(progress['stream'])
|
|
|
|
|
2017-05-25 22:15:00 +00:00
|
|
|
|
|
|
|
class LegacyBinderDockerBuildPack(DockerBuildPack):
|
2017-05-28 01:15:03 +00:00
|
|
|
|
2017-05-25 22:15:00 +00:00
|
|
|
name = Unicode('Legacy Binder Dockerfile')
|
2017-05-28 01:15:03 +00:00
|
|
|
|
2017-05-25 22:37:33 +00:00
|
|
|
dockerfile_appendix = Unicode(dedent(r"""
|
|
|
|
USER root
|
|
|
|
COPY . /home/main/notebooks
|
|
|
|
RUN chown -R main:main /home/main/notebooks
|
|
|
|
USER main
|
|
|
|
WORKDIR /home/main/notebooks
|
|
|
|
ENV PATH /home/main/anaconda2/envs/python3/bin:$PATH
|
|
|
|
RUN conda install -n python3 notebook==5.0.0 ipykernel==4.6.0 && \
|
|
|
|
pip install jupyterhub==0.7.2 && \
|
|
|
|
conda remove -n python3 nb_conda_kernels && \
|
|
|
|
conda install -n root ipykernel==4.6.0 && \
|
|
|
|
/home/main/anaconda2/envs/python3/bin/ipython kernel install --sys-prefix && \
|
|
|
|
/home/main/anaconda2/bin/ipython kernel install --prefix=/home/main/anaconda2/envs/python3
|
|
|
|
ENV JUPYTER_PATH /home/main/anaconda2/share/jupyter:$JUPYTER_PATH
|
|
|
|
CMD jupyter notebook --ip 0.0.0.0
|
|
|
|
"""), config=True)
|
2017-05-28 01:15:03 +00:00
|
|
|
|
2017-05-25 22:15:00 +00:00
|
|
|
def detect(self, workdir):
|
|
|
|
dockerfile = os.path.join(workdir, 'Dockerfile')
|
|
|
|
if not os.path.exists(dockerfile):
|
|
|
|
return False
|
|
|
|
with open(dockerfile, 'r') as f:
|
|
|
|
for line in f:
|
|
|
|
if line.startswith('FROM'):
|
|
|
|
if 'andrewosh/binder-base' in line.split('#')[0].lower():
|
|
|
|
self.amend_dockerfile(dockerfile)
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
# No FROM?!
|
|
|
|
return False
|
|
|
|
|
|
|
|
def amend_dockerfile(self, dockerfile):
|
|
|
|
with open(dockerfile, 'a') as f:
|
2017-05-25 22:37:33 +00:00
|
|
|
f.write(self.dockerfile_appendix)
|
2017-05-09 20:39:03 +00:00
|
|
|
|
|
|
|
|
2017-05-22 16:47:08 +00:00
|
|
|
class S2IBuildPack(BuildPack):
|
2017-05-23 19:09:48 +00:00
|
|
|
# Simple subclasses of S2IBuildPack must set build_image,
|
|
|
|
# either via config or during `detect()`
|
|
|
|
build_image = Unicode('')
|
|
|
|
|
2017-05-22 16:47:08 +00:00
|
|
|
def s2i_build(self, workdir, ref, output_image_spec, build_image):
|
|
|
|
# Note: Ideally we'd just copy from workdir here, rather than clone and check out again
|
|
|
|
# However, setting just --copy and not specifying a ref seems to check out master for
|
|
|
|
# some reason. Investigate deeper FIXME
|
|
|
|
cmd = [
|
|
|
|
's2i',
|
|
|
|
'build',
|
|
|
|
'--exclude', '""',
|
|
|
|
'--ref', ref,
|
2017-05-22 22:11:48 +00:00
|
|
|
'.',
|
2017-05-22 16:47:08 +00:00
|
|
|
build_image,
|
|
|
|
output_image_spec,
|
|
|
|
]
|
2017-05-24 16:49:23 +00:00
|
|
|
env = os.environ.copy()
|
2017-05-24 18:47:13 +00:00
|
|
|
# add bundled s2i to *end* of PATH,
|
|
|
|
# in case user doesn't have s2i
|
|
|
|
env['PATH'] = os.pathsep.join([env.get('PATH') or os.defpath, here])
|
2017-05-22 16:47:08 +00:00
|
|
|
try:
|
2017-05-24 21:11:37 +00:00
|
|
|
for line in execute_cmd(cmd, cwd=workdir, env=env, capture=self.capture):
|
2017-05-22 16:47:08 +00:00
|
|
|
self.log.info(line, extra=dict(phase='building', builder=self.name))
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
self.log.error('Failed to build image!', extra=dict(phase='failed'))
|
|
|
|
sys.exit(1)
|
2017-05-23 19:09:48 +00:00
|
|
|
|
|
|
|
def build(self, workdir, ref, output_image_spec):
|
|
|
|
return self.s2i_build(workdir, ref, output_image_spec, self.build_image)
|
2017-05-22 16:47:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CondaBuildPack(S2IBuildPack):
|
|
|
|
"""Build Pack for installing from a conda environment.yml using S2I"""
|
|
|
|
|
|
|
|
name = Unicode('conda')
|
|
|
|
build_image = Unicode('jupyterhub/singleuser-builder-conda:v0.1.5', config=True)
|
|
|
|
|
|
|
|
def detect(self, workdir):
|
|
|
|
return os.path.exists(os.path.join(workdir, 'environment.yml'))
|
|
|
|
|
|
|
|
|
|
|
|
class PythonBuildPack(S2IBuildPack):
|
|
|
|
"""Build Pack for installing from a pip requirements.txt using S2I"""
|
2017-05-16 01:54:51 +00:00
|
|
|
name = Unicode('python-pip')
|
2017-05-09 08:37:19 +00:00
|
|
|
runtime_builder_map = Dict({
|
2017-05-10 01:30:46 +00:00
|
|
|
'python-2.7': 'jupyterhub/singleuser-builder-venv-2.7:v0.1.5',
|
|
|
|
'python-3.5': 'jupyterhub/singleuser-builder-venv-3.5:v0.1.5',
|
2017-05-09 08:37:19 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
runtime = Unicode(
|
|
|
|
'python-3.5',
|
|
|
|
config=True
|
|
|
|
)
|
|
|
|
|
|
|
|
def detect(self, workdir):
|
|
|
|
if os.path.exists(os.path.join(workdir, 'requirements.txt')):
|
|
|
|
try:
|
|
|
|
with open(os.path.join(workdir, 'runtime.txt')) as f:
|
|
|
|
self.runtime = f.read().strip()
|
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
2017-05-23 19:09:48 +00:00
|
|
|
self.build_image = self.runtime_builder_map[self.runtime]
|
2017-05-09 08:37:19 +00:00
|
|
|
return True
|