7
0
mirror of https://github.com/ChristianLight/tutor.git synced 2024-06-09 17:22:25 +00:00
tutor/tests/commands/test_images.py
Régis Behmo 18ce1f2fe4 feat: persistent bind-mounts
This is an important change, where we get remove the previous `--mount`
option, and instead opt for persistent bind-mounts.

Persistent bind mounts have several advantages:
- They make it easier to remember which folders need to be bind-mounted.
- Code is *much* less clunky, as we no longer need to generate temporary
  docker-compose files.
- They allow us to bind-mount host directories *at build time* using the
  buildx `--build-context` option.
- The transition from development to production becomes much easier, as
  images will automatically be built using the host repo.

The only drawback is that persistent bind-mounts are slightly less
portable: when a config.yml file is moved to a different folder, many
things will break if the repo is not checked out in the same path.

For instance, this is how to start working on a local fork of
edx-platform:

    tutor config save --append MOUNTS=/path/to/edx-platform

And that's all there is to it. No, this fork will be used whenever we
run:

    tutor images build openedx
    tutor local start
    tutor dev start

This change is made possible by huge improvements in the build time
performance. These improvements make it convenient to re-build Docker
images often.

Related issues:
https://github.com/openedx/wg-developer-experience/issues/71
https://github.com/openedx/wg-developer-experience/issues/66
https://github.com/openedx/wg-developer-experience/issues/166
2023-06-14 21:08:49 +02:00

159 lines
5.6 KiB
Python

from unittest.mock import Mock, patch
from tests.helpers import PluginsTestCase, temporary_root
from tutor import images, plugins, utils
from tutor.__about__ import __version__
from tutor.commands.images import ImageNotFoundError
from .base import TestCommandMixin
class ImagesTests(PluginsTestCase, TestCommandMixin):
def test_images_help(self) -> None:
result = self.invoke(["images", "--help"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
def test_images_pull_image(self) -> None:
result = self.invoke(["images", "pull"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
def test_images_pull_plugin_invalid_plugin_should_throw_error(self) -> None:
result = self.invoke(["images", "pull", "plugin"])
self.assertEqual(1, result.exit_code)
self.assertEqual(ImageNotFoundError, type(result.exception))
@patch.object(images, "pull", return_value=None)
def test_images_pull_plugin(self, image_pull: Mock) -> None:
plugins.v0.DictPlugin(
{
"name": "plugin1",
"hooks": {
"remote-image": {
"service1": "service1:1.0.0",
"service2": "service2:2.0.0",
}
},
}
)
plugins.load("plugin1")
result = self.invoke(["images", "pull", "service1"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
image_pull.assert_called_once_with("service1:1.0.0")
@patch.object(images, "pull", return_value=None)
def test_images_pull_all_vendor_images(self, image_pull: Mock) -> None:
result = self.invoke(["images", "pull", "mysql"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
# Note: we should update this tag whenever the mysql image is updated
image_pull.assert_called_once_with("docker.io/mysql:8.0.33")
def test_images_printtag_image(self) -> None:
result = self.invoke(["images", "printtag", "openedx"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
self.assertRegex(
result.output, rf"docker.io/overhangio/openedx:{__version__}\n"
)
def test_images_printtag_plugin(self) -> None:
plugins.v0.DictPlugin(
{
"name": "plugin1",
"hooks": {
"build-image": {
"service1": "service1:1.0.0",
"service2": "service2:2.0.0",
}
},
}
)
plugins.load("plugin1")
result = self.invoke(["images", "printtag", "service1"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code, result)
self.assertEqual(result.output, "service1:1.0.0\n")
@patch.object(images, "build", return_value=None)
def test_images_build_plugin(self, mock_image_build: Mock) -> None:
plugins.v0.DictPlugin(
{
"name": "plugin1",
"hooks": {
"build-image": {
"service1": "service1:1.0.0",
"service2": "service2:2.0.0",
}
},
}
)
plugins.load("plugin1")
with temporary_root() as root:
self.invoke_in_root(root, ["config", "save"])
result = self.invoke_in_root(root, ["images", "build", "service1"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
mock_image_build.assert_called()
self.assertIn("service1:1.0.0", mock_image_build.call_args[0])
@patch.object(images, "build", return_value=None)
def test_images_build_plugin_with_args(self, image_build: Mock) -> None:
plugins.v0.DictPlugin(
{
"name": "plugin1",
"hooks": {
"build-image": {
"service1": "service1:1.0.0",
"service2": "service2:2.0.0",
}
},
}
)
plugins.load("plugin1")
build_args = [
"images",
"build",
"--no-cache",
"-a",
"myarg=value",
"--add-host",
"host",
"--target",
"target",
"-d",
"docker_args",
"service1",
]
with temporary_root() as root:
utils.is_buildkit_enabled.cache_clear()
with patch.object(utils, "is_buildkit_enabled", return_value=False):
self.invoke_in_root(root, ["config", "save"])
result = self.invoke_in_root(root, build_args)
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)
image_build.assert_called()
self.assertIn("service1:1.0.0", image_build.call_args[0])
self.assertEqual(
[
"service1:1.0.0",
"--no-cache",
"--build-arg",
"myarg=value",
"--add-host",
"host",
"--target",
"target",
"docker_args",
"--cache-from=type=registry,ref=service1:1.0.0-cache",
],
list(image_build.call_args[0][1:]),
)
def test_images_push(self) -> None:
result = self.invoke(["images", "push"])
self.assertIsNone(result.exception)
self.assertEqual(0, result.exit_code)