Source code for medikit.feature.docker

Adds docker capabilities to your package, using either "docker build" or "rocker build" to create an image containing
your code, in a fully functionnal python virtualenv.

from argparse import Namespace

import medikit
from import subscribe
from medikit.feature import Feature
from medikit.feature.make import which
from medikit.structs import Script

DEFAULT_NAME = "$(shell echo $(PACKAGE) | tr A-Z a-z)"

DOCKER = "docker"
ROCKER = "rocker"

[docs]class DockerConfig(Feature.Config): def __init__(self): self._registry = None self._user = None self._name = DEFAULT_NAME self._build_file = None self._compose_file = None self.use_default_builder()
[docs] def set_remote(self, registry=None, user=None, name=DEFAULT_NAME): self._registry = registry self._user = user self._name = name
@property def compose_file(self): if self._compose_file is None: return "docker-compose.yml" if self._compose_file is False: return None return self._compose_file @compose_file.setter def compose_file(self, value): self._compose_file = value @property def build_file(self): if self._build_file is None: return self.builder.title() + "file" return self._build_file @build_file.setter def build_file(self, value): self._build_file = value def _get_default_variables(self): return dict( DOCKER=which("docker"), USE_BUILDKIT="", DOCKER_BUILD="$(if $(USE_BUILDKIT),$(DOCKER) buildx build,$(DOCKER) image build)", DOCKER_BUILD_OPTIONS="--build-arg IMAGE=$(DOCKER_IMAGE) --build-arg TAG=$(DOCKER_TAG)", DOCKER_PUSH="$(DOCKER) image push", DOCKER_PUSH_OPTIONS="", DOCKER_RUN="$(DOCKER) run", DOCKER_RUN_COMMAND="", DOCKER_RUN_NAME="$(PACKAGE)_run", DOCKER_RUN_OPTIONS="", DOCKER_RUN_PORTS="", ) def _get_default_image_variables(self): return dict( DOCKER_BUILD_FILE="", # will be set at runtime. DOCKER_IMAGE="", # will be set at runtime, see #71. DOCKER_TAG="$(VERSION)", )
[docs] def disable_builder(self): self.builder = None self._variables = [] self.scripts = Namespace()
[docs] def use_default_builder(self): self.builder = DOCKER self._variables = [self._get_default_variables(), self._get_default_image_variables()] self.scripts = Namespace( build=Script( "$(DOCKER_BUILD) -f $(DOCKER_BUILD_FILE) $(DOCKER_BUILD_OPTIONS) -t $(DOCKER_IMAGE):$(DOCKER_TAG) .", doc="Build a docker image.", ), push=Script( "$(DOCKER_PUSH) $(DOCKER_PUSH_OPTIONS) $(DOCKER_IMAGE):$(DOCKER_TAG)", doc="Push docker image to remote registry.", ), run=Script( "$(DOCKER_RUN) $(DOCKER_RUN_OPTIONS) --interactive --tty --rm --name=$(DOCKER_RUN_NAME) $(DOCKER_RUN_PORTS) $(DOCKER_IMAGE):$(DOCKER_TAG) $(DOCKER_RUN_COMMAND)", doc="Run the default entry point in a container based on our docker image.", ), shell=Script( 'DOCKER_RUN_COMMAND="/bin/bash" $(MAKE) docker-run', doc="Run bash in a container based on our docker image.", ), )
[docs] def use_rocker_builder(self): self.use_default_builder() self.builder = ROCKER self._variables = [ self._get_default_variables(), self._get_default_image_variables(), dict( ROCKER=which("rocker"), ROCKER_BUILD="$(ROCKER) build", ROCKER_BUILD_OPTIONS="", ROCKER_BUILD_VARIABLES="--var DOCKER_IMAGE=$(DOCKER_IMAGE) --var DOCKER_TAG=$(DOCKER_TAG) --var PYTHON_REQUIREMENTS_FILE=requirements-prod.txt", ), ]"$(ROCKER_BUILD) $(ROCKER_BUILD_OPTIONS) $(ROCKER_BUILD_VARIABLES) .") self.scripts.push.set('ROCKER_BUILD_OPTIONS="$(ROCKER_BUILD_OPTIONS) --push" $(MAKE) docker-build')
@property def variables(self): for variables in self._variables: yield from variables.items() @property def image(self): return "/".join(filter(None, (self._registry, self._user, self._name)))
[docs]class DockerFeature(Feature): Config = DockerConfig
[docs] @subscribe("medikit.feature.make.on_generate", priority=-1) def on_make_generate(self, event): docker_config = event.config["docker"] for var, val in docker_config.variables: event.makefile[var] = val # Set DOCKER_IMAGE at runtime, see #71. if ("DOCKER_BUILD_FILE" in event.makefile) and not event.makefile["DOCKER_BUILD_FILE"]: event.makefile["DOCKER_BUILD_FILE"] = docker_config.build_file if ("DOCKER_IMAGE" in event.makefile) and not event.makefile["DOCKER_IMAGE"]: event.makefile["DOCKER_IMAGE"] = docker_config.image # Targets for script_name, script_content in sorted(docker_config.scripts.__dict__.items()): event.makefile.add_target("docker-" + script_name, script_content, phony=True, doc=script_content.doc)
[docs] @subscribe(medikit.on_end) def on_end(self, event): docker_config = event.config["docker"] self.render_file_inline( ".dockerignore", """ **/__pycache__ *.egg-info .cache .git .idea /Dockerfile /Projectfile /Rockerfile node_modules static """, event.variables, ) if docker_config.compose_file: self.render_file_inline( docker_config.compose_file, """ version: '3' volumes: # postgres_data: {} services: # postgres: # image: postgres:10 # ports: # - 5432:5432 # volumes: # - postgres_data:/var/lib/postgresql/data """, ) if docker_config.builder == DOCKER: self.render_file_inline( docker_config.build_file, """ FROM python:3 """, ) elif docker_config.builder == ROCKER: self.render_file_inline( docker_config.build_file, """ FROM python:3 # Mount cache volume to keep cache persistent from one build to another MOUNT /app/.cache WORKDIR /app # Create application user RUN useradd --home-dir /app --group www-data app \ && pip install -U pip wheel virtualenv \ && mkdir /env \ && chown app:www-data -R /app /env # Add and install python requirements in a virtualenv USER app RUN virtualenv -p python3 /env/ ADD *.txt /app/ RUN /env/bin/pip install -r {{ '{{ .PYTHON_REQUIREMENTS_FILE }}' }} # Add everything else USER root ADD . /app # IMPORT /static /app # IMPORT /assets.json /app RUN chown app:www-data -R /app # Entrypoint USER app CMD /env/bin/gunicorn config.wsgi --bind --workers 4 PUSH {{ '{{ .DOCKER_IMAGE }}:{{ .DOCKER_TAG }}' }} """, ) elif docker_config.builder is not None: raise NotImplementedError("Unknown builder {}".format(docker_config.builder))