Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supported pushing Docker image to a Docker registry #89

Merged
merged 10 commits into from
Dec 6, 2023
1 change: 1 addition & 0 deletions doc/changes/changes_0.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Version: 0.1.0
* #23: Fixed AWS Code build
* #69: Added entry point to start Jupyter server
* #84: Fixed retrieval and display of jupyter password
* #36: Supported pushing Docker image to a Docker registry

## Bug Fixes

Expand Down
9 changes: 9 additions & 0 deletions exasol/ds/sandbox/cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import click
import os


@click.group()
def cli():
pass


def option_with_env_default(envvar: str, *args, **kwargs):
kwargs["help"] = f"{kwargs['help']} [defaults to environment variable '{envvar}']"
return click.option(
*args, **kwargs,
default=lambda: os.environ.get(envvar, ""),
)
35 changes: 24 additions & 11 deletions exasol/ds/sandbox/cli/commands/create_docker_image.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import click

from exasol.ds.sandbox.cli.cli import cli
from exasol.ds.sandbox.cli.cli import cli, option_with_env_default
from exasol.ds.sandbox.cli.options.logging import logging_options
from exasol.ds.sandbox.cli.common import add_options
from exasol.ds.sandbox.lib.dss_docker import DssDockerImage
from exasol.ds.sandbox.lib.dss_docker import DssDockerImage, DockerRegistry
from exasol.ds.sandbox.lib.logging import SUPPORTED_LOG_LEVELS
from exasol.ds.sandbox.lib.logging import set_log_level

Expand All @@ -14,10 +14,15 @@
'--repository', type=str, metavar="ORG/REPO", show_default=True,
default="exasol/data-science-sandbox",
help="Organization and repository on hub.docker.com to publish the docker image to"),
click.option('--version', type=str, help="Docker image version tag"),
click.option(
'--publish', type=bool, is_flag=True,
help="Whether to publish the created Docker image"),
'--version', type=str, metavar="VERSION",
help="Docker image version tag"),
option_with_env_default("DOCKER_REGISTRY_USER",
'--registry-user', type=str, metavar="USER",
help="Username for publication to Docker registry"),
option_with_env_default("DOCKER_REGISTRY_PASSWORD",
ckunki marked this conversation as resolved.
Show resolved Hide resolved
'--registry-password', type=str, metavar="PASSWORD",
help="Password for publication to Docker registry"),
click.option(
'--keep-container', type=bool, is_flag=True,
help="""Keep the Docker Container running after creating the image.
Expand All @@ -27,18 +32,26 @@
def create_docker_image(
repository: str,
version: str,
publish: bool,
registry_user: str,
registry_password: str,
keep_container: bool,
log_level: str,
):
"""
Create a Docker image for data-science-sandbox and deploy
it to a Docker repository.
Create a Docker image for data-science-sandbox. If username and password
ckunki marked this conversation as resolved.
Show resolved Hide resolved
for the Docker registry are specified then deploy the image to the registry.
"""
set_log_level(log_level)
DssDockerImage(
creator = DssDockerImage(
repository=repository,
version=version,
publish=publish,
keep_container=keep_container,
).create()
)
if registry_user and registry_password:
creator.registry = DockerRegistry(
creator.repository,
registry_user,
registry_password,
)
print(f'user {creator.registry.username} password {registry_password}')
ckunki marked this conversation as resolved.
Show resolved Hide resolved
# creator.create()
2 changes: 1 addition & 1 deletion exasol/ds/sandbox/lib/dss_docker/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .create_image import DssDockerImage
from .create_image import DssDockerImage, DockerRegistry
17 changes: 13 additions & 4 deletions exasol/ds/sandbox/lib/dss_docker/create_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from exasol.ds.sandbox.lib.ansible.ansible_access import AnsibleAccess, AnsibleFacts
from exasol.ds.sandbox.lib.setup_ec2.run_install_dependencies import run_install_dependencies
from exasol.ds.sandbox.lib.setup_ec2.host_info import HostInfo

from exasol.ds.sandbox.lib.dss_docker.push_image import DockerRegistry

DSS_VERSION = version("exasol-data-science-sandbox")
_logger = get_status_logger(LogType.DOCKER_IMAGE)
Expand Down Expand Up @@ -63,15 +63,19 @@ def __init__(
self,
repository: str,
version: str = None,
publish: bool = False,
keep_container: bool = False,
):
version = version if version else DSS_VERSION
self.container_name = f"ds-sandbox-{DssDockerImage.timestamp()}"
self.image_name = f"{repository}:{version}"
self.publish = publish
self.repository = repository
self.version = version
self.keep_container = keep_container
self._start = None
self.registry = None

@property
def image_name(self):
return f"{self.repository}:{self.version}"

def _ansible_run_context(self) -> AnsibleRunContext:
extra_vars = {
Expand Down Expand Up @@ -150,11 +154,16 @@ def _cleanup(self, container: DockerContainer):
_logger.info("Removing container")
container.remove()

def _push(self):
if self.registry is not None:
self.registry.push(self.version)

def create(self):
try:
container = self._start_container()
facts = self._install_dependencies()
image = self._commit_container(container, facts)
self._push()
except Exception as ex:
raise ex
finally:
Expand Down
79 changes: 79 additions & 0 deletions exasol/ds/sandbox/lib/dss_docker/push_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import docker
import json
import logging
import requests

from docker.client import DockerClient

from typing import Callable, Dict, Optional
from exasol.ds.sandbox.lib.logging import get_status_logger, LogType


_logger = get_status_logger(LogType.DOCKER_IMAGE)


def get_from_dict(d: Dict[str, any], *keys: str) -> str:
ckunki marked this conversation as resolved.
Show resolved Hide resolved
for key in keys:
if not key in d:
return None
d = d[key]
return d
ckunki marked this conversation as resolved.
Show resolved Hide resolved


class ProgressReporter:
def __init__(self, verbose: bool):
self.last_status = None
self.verbose = verbose
self.need_linefeed = False

def _report(self, printer: Callable, msg: Optional[str], **kwargs):
if msg is not None:
printer(msg, **kwargs)

def _linefeed(self):
if self.need_linefeed:
self.need_linefeed = False
print()

def report(self, status: Optional[str], progress: Optional[str]):
if not self.verbose:
return
if status == self.last_status:
self._report(print, progress, end="\r")
self.need_linefeed = progress
else:
self.last_status = status
self._linefeed()
self._report(_logger.info, status)
ckunki marked this conversation as resolved.
Show resolved Hide resolved


class DockerRegistry:
def __init__(self, repository: str, username: str, password: str):
self.repository = repository
self.username = username
self.password = password
self._client = None

def client(self):
ckunki marked this conversation as resolved.
Show resolved Hide resolved
if self._client is None:
self._client = docker.from_env()
return self._client

def push(self, tag: str):
auth_config = {
"username": self.username,
"password": self.password,
}
resp = self.client().images.push(
repository=self.repository,
tag=tag,
auth_config=auth_config,
stream=True,
decode=True,
)
reporter = ProgressReporter(_logger.isEnabledFor(logging.INFO))
for el in resp:
reporter.report(
el.get("status", None),
el.get("progress", None),
)
4 changes: 2 additions & 2 deletions exasol/ds/sandbox/runtime/ansible/dss_docker_playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
need_sudo: false
docker_integration_test: true
tasks:
- import_tasks: general_setup_tasks.yml
- import_tasks: cleanup_tasks.yml
# - import_tasks: general_setup_tasks.yml
# - import_tasks: cleanup_tasks.yml
ckunki marked this conversation as resolved.
Show resolved Hide resolved
35 changes: 35 additions & 0 deletions test/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
import docker
import pytest

from exasol.ds.sandbox.lib.dss_docker import DssDockerImage


def pytest_addoption(parser):
parser.addoption(
"--dss-docker-image", default=None,
help="Name and version of existing Docker image to use for tests",
)
parser.addoption(
ckunki marked this conversation as resolved.
Show resolved Hide resolved
"--docker-registry", default=None, metavar="HOST:PORT",
help="Docker registry for pushing Docker images to",
)


@pytest.fixture(scope="session")
def dss_docker_image(request):
ckunki marked this conversation as resolved.
Show resolved Hide resolved
"""
If dss_docker_image_name is provided then don't create an image but
reuse the existing image as specified by cli option
--ds-docker-image-name.
"""
existing = request.config.getoption("--dss-docker-image")
if existing and ":" in existing:
name, version = existing.split(":")
yield DssDockerImage(name, version)
return

testee = DssDockerImage(
"my-repo/dss-test-image",
version=f"{DssDockerImage.timestamp()}",
publish=False,
keep_container=False,
)
testee.create()
try:
yield testee
finally:
docker.from_env().images.remove(testee.image_name)
26 changes: 0 additions & 26 deletions test/integration/test_create_dss_docker_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,6 @@
from exasol.ds.sandbox.lib import pretty_print


@pytest.fixture(scope="session")
def dss_docker_image(request):
"""
If dss_docker_image_name is provided then don't create an image but
reuse the existing image as specified by cli option
--ds-docker-image-name, see file conftest.py.
"""
existing = request.config.getoption("--dss-docker-image")
if existing and ":" in existing:
name, version = existing.split(":")
yield DssDockerImage(name, version)
return

testee = DssDockerImage(
"my-repo/dss-test-image",
version=f"{DssDockerImage.timestamp()}",
publish=False,
keep_container=False,
)
testee.create()
try:
yield testee
finally:
docker.from_env().images.remove(testee.image_name)


@pytest.fixture
def dss_docker_container(dss_docker_image):
client = docker.from_env()
Expand Down
Loading
Loading