Skip to content

Commit 3b6b8e6

Browse files
committed
Using extras_require for requirements for Docker and Vagrant backends
1 parent 4294e98 commit 3b6b8e6

File tree

9 files changed

+123
-45
lines changed

9 files changed

+123
-45
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ after_success:
2929
- codecov
3030
branches:
3131
only:
32-
master
32+
- master
33+
- /release-v[0-9]+/
3334
deploy:
3435
provider: script
3536
skip_cleanup: true

README.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,25 @@ To install the last stable version:
5252
5353
python -m pip install arca
5454
55+
If you want to use the Docker backend:
56+
57+
.. code-block:: bash
58+
59+
python -m pip install arca[docker]
60+
61+
Or if you want to use the Vagrant backend:
62+
63+
.. code-block:: bash
64+
65+
python -m pip install arca[vagrant]
66+
5567
Or if you wish to install the upstream version:
5668

5769
.. code-block:: bash
5870
5971
python -m pip install git+https://github.com/mikicz/arca.git#egg=arca
72+
python -m pip install git+https://github.com/mikicz/arca.git#egg=arca[docker]
73+
python -m pip install git+https://github.com/mikicz/arca.git#egg=arca[vagrant]
6074
6175
Example
6276
+++++++

arca/backend/docker.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
from pathlib import Path
1010
from typing import Optional, List, Tuple, Union, Callable
1111

12-
import docker
13-
import docker.errors
14-
from docker.models.containers import Container, ExecResult
15-
from docker.models.images import Image
12+
try:
13+
import docker
14+
import docker.errors
15+
except ImportError:
16+
docker = None
17+
1618
from git import Repo
1719
from requests.exceptions import ConnectionError
1820

@@ -66,6 +68,9 @@ class DockerBackend(BaseBackend):
6668
def __init__(self, **kwargs):
6769
super().__init__(**kwargs)
6870

71+
if docker is None:
72+
raise ArcaMisconfigured(ArcaMisconfigured.PACKAGE_MISSING.format("docker"))
73+
6974
self._containers = set()
7075
self.client = None
7176

@@ -367,12 +372,14 @@ def get_inherit_image(self) -> Tuple[str, str]:
367372

368373
def build_image_from_inherited_image(self, image_name: str, image_tag: str,
369374
build_context: Path,
370-
requirements_file: Optional[Path]) -> Image:
375+
requirements_file: Optional[Path]):
371376
"""
372377
Builds a image with installed requirements from the inherited image. (Or just tags the image
373378
if there are no requirements.)
374379
375380
See :meth:`build_image` for parameters descriptions.
381+
382+
:rtype: docker.models.images.Image
376383
"""
377384

378385
base_name, base_tag = self.get_inherit_image()
@@ -429,7 +436,7 @@ def install_dependencies_dockerfile():
429436
def build_image(self, image_name: str, image_tag: str,
430437
build_context: Path,
431438
requirements_file: Optional[Path],
432-
dependencies: Optional[List[str]]) -> Image:
439+
dependencies: Optional[List[str]]):
433440
""" Builds an image for specific requirements and dependencies, based on the settings.
434441
435442
:param image_name: How the image should be named
@@ -438,6 +445,7 @@ def build_image(self, image_name: str, image_tag: str,
438445
:param requirements_file: Path to the requirements file in the repository (or ``None`` if it doesn't exist)
439446
:param dependencies: List of dependencies (in the formalized format)
440447
:return: The Image instance.
448+
:rtype: docker.models.images.Image
441449
"""
442450
if self.inherit_image is not None:
443451
return self.build_image_from_inherited_image(image_name, image_tag, build_context, requirements_file)
@@ -491,9 +499,10 @@ def install_requirements_dockerfile():
491499

492500
return self.get_image(image_name, image_tag)
493501

494-
def push_to_registry(self, image: Image, image_tag: str):
502+
def push_to_registry(self, image, image_tag: str):
495503
""" Pushes a local image to a registry based on the ``use_registry_name`` setting.
496504
505+
:type image: docker.models.images.Image
497506
:raise PushToRegistryError: If the push fails.
498507
"""
499508
# already tagged, so it's already pushed
@@ -530,21 +539,24 @@ def image_exists(self, image_name, image_tag):
530539
except docker.errors.ImageNotFound:
531540
return False
532541

533-
def get_image(self, image_name, image_tag) -> Image:
542+
def get_image(self, image_name, image_tag):
534543
""" Returns a :class:`Image <docker.models.images.Image>` instance for the provided name and tag.
544+
545+
:rtype: docker.models.images.Image
535546
"""
536547
return self.client.images.get(f"{image_name}:{image_tag}")
537548

538-
def try_pull_image_from_registry(self, image_name, image_tag) -> Optional[Image]:
549+
def try_pull_image_from_registry(self, image_name, image_tag):
539550
"""
540551
Tries to pull a image with the tag ``image_tag`` from registry set by ``use_registry_name``.
541552
After the image is pulled, it's tagged with ``image_name``:``image_tag`` so lookup can
542553
be made locally next time.
543554
544555
:return: A :class:`Image <docker.models.images.Image>` instance if the image exists, ``None`` otherwise.
556+
:rtype: Optional[docker.models.images.Image]
545557
"""
546558
try:
547-
image: Image = self.client.images.pull(self.use_registry_name, image_tag)
559+
image = self.client.images.pull(self.use_registry_name, image_tag)
548560
except (docker.errors.ImageNotFound, docker.errors.NotFound): # the image doesn't exist
549561
logger.info("Tried to pull %s:%s from a registry, not found", self.use_registry_name, image_tag)
550562
return None
@@ -557,11 +569,12 @@ def try_pull_image_from_registry(self, image_name, image_tag) -> Optional[Image]
557569

558570
return image
559571

560-
def container_running(self, container_name) -> Optional[Container]:
572+
def container_running(self, container_name):
561573
"""
562574
Finds out if a container with name ``container_name`` is running.
563575
564576
:return: :class:`Container <docker.models.containers.Container>` if it's running, ``None`` otherwise.
577+
:rtype: Optional[docker.models.container.Container]
565578
"""
566579
filters = {
567580
"name": container_name,
@@ -618,8 +631,11 @@ def tar_task_definition(self, name: str, contents: str) -> bytes:
618631

619632
return tarstream.getvalue()
620633

621-
def start_container(self, image: Image, container_name: str, repo_path: Path) -> Container:
634+
def start_container(self, image, container_name: str, repo_path: Path):
622635
""" Starts a container with the image and name ``container_name`` and copies the repository into the container.
636+
637+
:type image: docker.models.images.Image
638+
:rtype: docker.models.container.Container
623639
"""
624640
container = self.client.containers.run(image, command="bash -i", detach=True, tty=True, name=container_name,
625641
working_dir=str((Path("/srv/data") / self.cwd).resolve()),
@@ -631,7 +647,7 @@ def start_container(self, image: Image, container_name: str, repo_path: Path) ->
631647

632648
return container
633649

634-
def get_image_for_repo(self, repo: str, branch: str, git_repo: Repo, repo_path: Path) -> Image:
650+
def get_image_for_repo(self, repo: str, branch: str, git_repo: Repo, repo_path: Path):
635651
"""
636652
Returns an image for the specific repo (based on settings and requirements).
637653
@@ -641,6 +657,8 @@ def get_image_for_repo(self, repo: str, branch: str, git_repo: Repo, repo_path:
641657
4. Pushes the image to registry so the image is available next time (if ``registry_pull_only`` is not set)
642658
643659
See :meth:`run` for parameters descriptions.
660+
661+
:rtype: docker.models.images.Image
644662
"""
645663
requirements_file = self.get_requirements_file(repo_path)
646664
dependencies = self.get_dependencies()
@@ -703,7 +721,7 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
703721

704722
container.put_archive("/srv/scripts", self.tar_task_definition(task_filename, task_json))
705723

706-
res: Optional[ExecResult] = None
724+
res = None
707725

708726
try:
709727
res = container.exec_run(["python",

arca/backend/vagrant.py

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from textwrap import dedent
66
from uuid import uuid4
77

8-
from fabric import api
98
from git import Repo
109

1110
from arca.exceptions import ArcaMisconfigured, BuildError
@@ -15,17 +14,6 @@
1514
from .docker import DockerBackend
1615

1716

18-
@api.task
19-
def run_script(container_name, definition_filename):
20-
""" Sequence to run inside the VM, copies data and script to the container and runs the script.
21-
"""
22-
api.run(f"docker exec {container_name} mkdir -p /srv/scripts")
23-
api.run(f"docker cp /srv/data {container_name}:/srv")
24-
api.run(f"docker cp /vagrant/runner.py {container_name}:/srv/scripts/")
25-
api.run(f"docker cp /vagrant/{definition_filename} {container_name}:/srv/scripts/")
26-
return api.run(f"docker exec -t {container_name} python /srv/scripts/runner.py /srv/scripts/{definition_filename}")
27-
28-
2917
class VagrantBackend(DockerBackend):
3018
""" Uses Docker in Vagrant.
3119
@@ -53,6 +41,27 @@ class VagrantBackend(DockerBackend):
5341
quiet = LazySettingProperty(key="quiet", default=True, convert=bool)
5442
destroy = LazySettingProperty(key="destroy", default=True, convert=bool)
5543

44+
def __init__(self, **kwargs):
45+
super().__init__(**kwargs)
46+
47+
try:
48+
import docker # noqa: F401
49+
except ImportError:
50+
raise ArcaMisconfigured(ArcaMisconfigured.PACKAGE_MISSING.format("docker"))
51+
52+
try:
53+
import vagrant
54+
except ImportError:
55+
raise ArcaMisconfigured(ArcaMisconfigured.PACKAGE_MISSING.format("python-vagrant"))
56+
57+
try:
58+
import fabric # noqa: F401
59+
except ImportError:
60+
raise ArcaMisconfigured(ArcaMisconfigured.PACKAGE_MISSING.format("fabric3"))
61+
62+
if vagrant.get_vagrant_executable() is None:
63+
raise ArcaMisconfigured("Vagrant executable is not accessible!")
64+
5665
def validate_settings(self):
5766
""" Runs :meth:`arca.DockerBackend.validate_settings` and checks extra:
5867
@@ -74,15 +83,6 @@ def validate_settings(self):
7483
if self.registry_pull_only:
7584
raise ArcaMisconfigured("Push must be enabled for VagrantBackend")
7685

77-
def check_vagrant_access(self):
78-
"""
79-
:raise BuildError: If Vagrant is not installed.
80-
"""
81-
from vagrant import get_vagrant_executable
82-
83-
if get_vagrant_executable() is None:
84-
raise BuildError("Vagrant executable is not accessible!")
85-
8686
def get_vagrant_file_location(self, repo: str, branch: str, git_repo: Repo, repo_path: Path) -> Path:
8787
""" Returns a directory where Vagrantfile should be. Based on repo, branch and tag of the used docker image.
8888
"""
@@ -142,13 +142,33 @@ def create_vagrant_file(self, repo: str, branch: str, git_repo: Repo, repo_path:
142142

143143
(vagrant_file.parent / "runner.py").write_text(self._arca.RUNNER.read_text())
144144

145+
@property
146+
def fabric_task(self):
147+
""" Returns a fabric task which executes the script in the Vagrant VM
148+
"""
149+
from fabric import api
150+
151+
@api.task
152+
def run_script(container_name, definition_filename):
153+
""" Sequence to run inside the VM, copies data and script to the container and runs the script.
154+
"""
155+
api.run(f"docker exec {container_name} mkdir -p /srv/scripts")
156+
api.run(f"docker cp /srv/data {container_name}:/srv")
157+
api.run(f"docker cp /vagrant/runner.py {container_name}:/srv/scripts/")
158+
api.run(f"docker cp /vagrant/{definition_filename} {container_name}:/srv/scripts/")
159+
return api.run(" ".join([
160+
"docker", "exec", "-t", container_name,
161+
"python", "/srv/scripts/runner.py", f"/srv/scripts/{definition_filename}",
162+
]))
163+
164+
return run_script
165+
145166
def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Path):
146167
""" Gets or creates Vagrantfile, starts up a VM with it, executes Fabric script over SSH, returns result.
147168
"""
148169
# importing here, prints out warning when vagrant is missing even when the backend is not used otherwise
149170
from vagrant import Vagrant, make_file_cm
150-
151-
self.check_vagrant_access()
171+
from fabric import api
152172

153173
vagrant_file = self.get_vagrant_file_location(repo, branch, git_repo, repo_path)
154174

@@ -191,7 +211,7 @@ def run(self, repo: str, branch: str, task: Task, git_repo: Repo, repo_path: Pat
191211
api.output.everything = True
192212

193213
try:
194-
res = api.execute(run_script, container_name=container_name, definition_filename=task_filename)
214+
res = api.execute(self.fabric_task, container_name=container_name, definition_filename=task_filename)
195215

196216
return Result(json.loads(res[vagrant.user_hostname_port()].stdout))
197217
except BuildError: # can be raised by :meth:`Result.__init__`

arca/exceptions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class ArcaMisconfigured(ValueError, ArcaException):
99
""" An exception for all cases of misconfiguration.
1010
"""
1111

12+
PACKAGE_MISSING = "Couldn't import package '{}' that is required for this backend. " \
13+
"Did you install the extra requirements for this backend?"
14+
1215

1316
class TaskMisconfigured(ValueError, ArcaException):
1417
""" Raised if Task is incorrectly defined.

docs/backends.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Settings:
128128
* **disable_pull**: Disable pulling prebuilt arca images from Docker Hub and build even the base images locally.
129129
* **inherit_image**: If you don't wish to use the arca images you can specify what image should be used instead.
130130
* **use_registry_name**: Uses this registry to store images with installed requirements and dependencies to,
131-
tries to pull image from the registry before building it locally to save time.
131+
tries to pull image from the registry before building it locally to save time.
132132
* **registry_pull_only**: Disables pushing to registry.
133133

134134
(possible settings prefixes: ``ARCA_DOCKER_BACKEND_`` and ``ARCA_BACKEND_``)

docs/install.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,22 @@ To install the last stable version:
2424
2525
python -m pip install arca
2626
27+
If you want to use the Docker backend:
28+
29+
.. code-block:: bash
30+
31+
python -m pip install arca[docker]
32+
33+
Or if you want to use the Vagrant backend:
34+
35+
.. code-block:: bash
36+
37+
python -m pip install arca[vagrant]
38+
2739
Or if you wish to install the upstream version:
2840

2941
.. code-block:: bash
3042
3143
python -m pip install git+https://github.com/mikicz/arca.git#egg=arca
44+
python -m pip install git+https://github.com/mikicz/arca.git#egg=arca[docker]
45+
python -m pip install git+https://github.com/mikicz/arca.git#egg=arca[vagrant]

setup.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,19 @@ def long_description():
3434
"gitpython==2.1.9",
3535
"dogpile.cache==0.6.4",
3636
"requests",
37-
"docker~=3.1.0",
38-
"python-vagrant",
39-
"fabric3",
4037
"entrypoints>=0.2.3",
4138
"cached-property",
4239
],
40+
extras_require={
41+
"docker": [
42+
"docker~=3.1.0",
43+
],
44+
"vagrant": [
45+
"docker~=3.1.0",
46+
"python-vagrant",
47+
"fabric3",
48+
],
49+
},
4350
classifiers=[
4451
"Development Status :: 3 - Alpha",
4552
"Intended Audience :: Developers",

tests/test_vagrant.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99

1010
def test_validation():
11-
""" These tests work on Travis
12-
"""
11+
if os.environ.get("TRAVIS", False):
12+
pytest.skip("Vagrant doesn't work on Travis")
13+
1314
backend = VagrantBackend()
1415

1516
# VagrantBackend requires `push_to_registry`

0 commit comments

Comments
 (0)