Преглед изворни кода

Merge pull request #3612 from dnephin/tests_for_bundle

Add some unit tests and an acceptance test for bundle
Aanand Prasad пре 9 година
родитељ
комит
72d3d5d84b

+ 18 - 20
compose/bundle.py

@@ -57,17 +57,7 @@ class MissingDigests(Exception):
 
 
 def serialize_bundle(config, image_digests):
-    if config.networks:
-        log.warn("Unsupported top level key 'networks' - ignoring")
-
-    if config.volumes:
-        log.warn("Unsupported top level key 'volumes' - ignoring")
-
-    return json.dumps(
-        to_bundle(config, image_digests),
-        indent=2,
-        sort_keys=True,
-    )
+    return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)
 
 
 def get_image_digests(project, allow_fetch=False):
@@ -99,7 +89,7 @@ def get_image_digest(service, allow_fetch=False):
             "required to generate a proper image digest for the bundle. Specify "
             "an image repo and tag with the 'image' option.".format(s=service))
 
-    separator = parse_repository_tag(service.options['image'])[2]
+    _, _, separator = parse_repository_tag(service.options['image'])
     # Compose file already uses a digest, no lookup required
     if separator == '@':
         return service.options['image']
@@ -143,24 +133,32 @@ def fetch_image_digest(service):
     if not digest:
         raise ValueError("Failed to get digest for %s" % service.name)
 
-    repo = parse_repository_tag(service.options['image'])[0]
+    repo, _, _ = parse_repository_tag(service.options['image'])
     identifier = '{repo}@{digest}'.format(repo=repo, digest=digest)
 
-    # Pull by digest so that image['RepoDigests'] is populated for next time
-    # and we don't have to pull/push again
-    service.client.pull(identifier)
-
-    log.info("Stored digest for {}".format(service.image_name))
+    # only do this if RepoDigests isn't already populated
+    image = service.image()
+    if not image['RepoDigests']:
+        # Pull by digest so that image['RepoDigests'] is populated for next time
+        # and we don't have to pull/push again
+        service.client.pull(identifier)
+        log.info("Stored digest for {}".format(service.image_name))
 
     return identifier
 
 
 def to_bundle(config, image_digests):
+    if config.networks:
+        log.warn("Unsupported top level key 'networks' - ignoring")
+
+    if config.volumes:
+        log.warn("Unsupported top level key 'volumes' - ignoring")
+
     config = denormalize_config(config)
 
     return {
-        'version': VERSION,
-        'services': {
+        'Version': VERSION,
+        'Services': {
             name: convert_service_to_bundle(
                 name,
                 service_dict,

+ 27 - 0
tests/acceptance/cli_test.py

@@ -12,6 +12,7 @@ from collections import Counter
 from collections import namedtuple
 from operator import attrgetter
 
+import py
 import yaml
 from docker import errors
 
@@ -378,6 +379,32 @@ class CLITestCase(DockerClientTestCase):
         ]
         assert not containers
 
+    def test_bundle_with_digests(self):
+        self.base_dir = 'tests/fixtures/bundle-with-digests/'
+        tmpdir = py.test.ensuretemp('cli_test_bundle')
+        self.addCleanup(tmpdir.remove)
+        filename = str(tmpdir.join('example.dab'))
+
+        self.dispatch(['bundle', '--output', filename])
+        with open(filename, 'r') as fh:
+            bundle = json.load(fh)
+
+        assert bundle == {
+            'Version': '0.1',
+            'Services': {
+                'web': {
+                    'Image': ('dockercloud/hello-world@sha256:fe79a2cfbd17eefc3'
+                              '44fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d'),
+                    'Networks': ['default'],
+                },
+                'redis': {
+                    'Image': ('redis@sha256:a84cb8f53a70e19f61ff2e1d5e73fb7ae62d'
+                              '374b2b7392de1e7d77be26ef8f7b'),
+                    'Networks': ['default'],
+                }
+            },
+        }
+
     def test_create(self):
         self.dispatch(['create'])
         service = self.project.get_service('simple')

+ 9 - 0
tests/fixtures/bundle-with-digests/docker-compose.yml

@@ -0,0 +1,9 @@
+
+version: '2.0'
+
+services:
+  web:
+    image: dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d
+
+  redis:
+    image: redis@sha256:a84cb8f53a70e19f61ff2e1d5e73fb7ae62d374b2b7392de1e7d77be26ef8f7b

+ 232 - 0
tests/unit/bundle_test.py

@@ -0,0 +1,232 @@
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import docker
+import mock
+import pytest
+
+from compose import bundle
+from compose import service
+from compose.cli.errors import UserError
+from compose.config.config import Config
+
+
[email protected]
+def mock_service():
+    return mock.create_autospec(
+        service.Service,
+        client=mock.create_autospec(docker.Client),
+        options={})
+
+
+def test_get_image_digest_exists(mock_service):
+    mock_service.options['image'] = 'abcd'
+    mock_service.image.return_value = {'RepoDigests': ['digest1']}
+    digest = bundle.get_image_digest(mock_service)
+    assert digest == 'digest1'
+
+
+def test_get_image_digest_image_uses_digest(mock_service):
+    mock_service.options['image'] = image_id = 'redis@sha256:digest'
+
+    digest = bundle.get_image_digest(mock_service)
+    assert digest == image_id
+    assert not mock_service.image.called
+
+
+def test_get_image_digest_no_image(mock_service):
+    with pytest.raises(UserError) as exc:
+        bundle.get_image_digest(service.Service(name='theservice'))
+
+    assert "doesn't define an image tag" in exc.exconly()
+
+
+def test_fetch_image_digest_for_image_with_saved_digest(mock_service):
+    mock_service.options['image'] = image_id = 'abcd'
+    mock_service.pull.return_value = expected = 'sha256:thedigest'
+    mock_service.image.return_value = {'RepoDigests': ['digest1']}
+
+    digest = bundle.fetch_image_digest(mock_service)
+    assert digest == image_id + '@' + expected
+
+    mock_service.pull.assert_called_once_with()
+    assert not mock_service.push.called
+    assert not mock_service.client.pull.called
+
+
+def test_fetch_image_digest_for_image(mock_service):
+    mock_service.options['image'] = image_id = 'abcd'
+    mock_service.pull.return_value = expected = 'sha256:thedigest'
+    mock_service.image.return_value = {'RepoDigests': []}
+
+    digest = bundle.fetch_image_digest(mock_service)
+    assert digest == image_id + '@' + expected
+
+    mock_service.pull.assert_called_once_with()
+    assert not mock_service.push.called
+    mock_service.client.pull.assert_called_once_with(digest)
+
+
+def test_fetch_image_digest_for_build(mock_service):
+    mock_service.options['build'] = '.'
+    mock_service.options['image'] = image_id = 'abcd'
+    mock_service.push.return_value = expected = 'sha256:thedigest'
+    mock_service.image.return_value = {'RepoDigests': ['digest1']}
+
+    digest = bundle.fetch_image_digest(mock_service)
+    assert digest == image_id + '@' + expected
+
+    mock_service.push.assert_called_once_with()
+    assert not mock_service.pull.called
+    assert not mock_service.client.pull.called
+
+
+def test_to_bundle():
+    image_digests = {'a': 'aaaa', 'b': 'bbbb'}
+    services = [
+        {'name': 'a', 'build': '.', },
+        {'name': 'b', 'build': './b'},
+    ]
+    config = Config(
+        version=2,
+        services=services,
+        volumes={'special': {}},
+        networks={'extra': {}})
+
+    with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log:
+        output = bundle.to_bundle(config, image_digests)
+
+    assert mock_log.mock_calls == [
+        mock.call("Unsupported top level key 'networks' - ignoring"),
+        mock.call("Unsupported top level key 'volumes' - ignoring"),
+    ]
+
+    assert output == {
+        'Version': '0.1',
+        'Services': {
+            'a': {'Image': 'aaaa', 'Networks': ['default']},
+            'b': {'Image': 'bbbb', 'Networks': ['default']},
+        }
+    }
+
+
+def test_convert_service_to_bundle():
+    name = 'theservice'
+    image_digest = 'thedigest'
+    service_dict = {
+        'ports': ['80'],
+        'expose': ['1234'],
+        'networks': {'extra': {}},
+        'command': 'foo',
+        'entrypoint': 'entry',
+        'environment': {'BAZ': 'ENV'},
+        'build': '.',
+        'working_dir': '/tmp',
+        'user': 'root',
+        'labels': {'FOO': 'LABEL'},
+        'privileged': True,
+    }
+
+    with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log:
+        config = bundle.convert_service_to_bundle(name, service_dict, image_digest)
+
+    mock_log.assert_called_once_with(
+        "Unsupported key 'privileged' in services.theservice - ignoring")
+
+    assert config == {
+        'Image': image_digest,
+        'Ports': [
+            {'Protocol': 'tcp', 'Port': 80},
+            {'Protocol': 'tcp', 'Port': 1234},
+        ],
+        'Networks': ['extra'],
+        'Command': ['entry', 'foo'],
+        'Env': ['BAZ=ENV'],
+        'WorkingDir': '/tmp',
+        'User': 'root',
+        'Labels': {'FOO': 'LABEL'},
+    }
+
+
+def test_set_command_and_args_none():
+    config = {}
+    bundle.set_command_and_args(config, [], [])
+    assert config == {}
+
+
+def test_set_command_and_args_from_command():
+    config = {}
+    bundle.set_command_and_args(config, [], "echo ok")
+    assert config == {'Args': ['echo', 'ok']}
+
+
+def test_set_command_and_args_from_entrypoint():
+    config = {}
+    bundle.set_command_and_args(config, "echo entry", [])
+    assert config == {'Command': ['echo', 'entry']}
+
+
+def test_set_command_and_args_from_both():
+    config = {}
+    bundle.set_command_and_args(config, "echo entry", ["extra", "arg"])
+    assert config == {'Command': ['echo', 'entry', "extra", "arg"]}
+
+
+def test_make_service_networks_default():
+    name = 'theservice'
+    service_dict = {}
+
+    with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log:
+        networks = bundle.make_service_networks(name, service_dict)
+
+    assert not mock_log.called
+    assert networks == ['default']
+
+
+def test_make_service_networks():
+    name = 'theservice'
+    service_dict = {
+        'networks': {
+            'foo': {
+                'aliases': ['one', 'two'],
+            },
+            'bar': {}
+        },
+    }
+
+    with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log:
+        networks = bundle.make_service_networks(name, service_dict)
+
+    mock_log.assert_called_once_with(
+        "Unsupported key 'aliases' in services.theservice.networks.foo - ignoring")
+    assert sorted(networks) == sorted(service_dict['networks'])
+
+
+def test_make_port_specs():
+    service_dict = {
+        'expose': ['80', '500/udp'],
+        'ports': [
+            '400:80',
+            '222',
+            '127.0.0.1:8001:8001',
+            '127.0.0.1:5000-5001:3000-3001'],
+    }
+    port_specs = bundle.make_port_specs(service_dict)
+    assert port_specs == [
+        {'Protocol': 'tcp', 'Port': 80},
+        {'Protocol': 'tcp', 'Port': 222},
+        {'Protocol': 'tcp', 'Port': 8001},
+        {'Protocol': 'tcp', 'Port': 3000},
+        {'Protocol': 'tcp', 'Port': 3001},
+        {'Protocol': 'udp', 'Port': 500},
+    ]
+
+
+def test_make_port_spec_with_protocol():
+    port_spec = bundle.make_port_spec("5000/udp")
+    assert port_spec == {'Protocol': 'udp', 'Port': 5000}
+
+
+def test_make_port_spec_default_protocol():
+    port_spec = bundle.make_port_spec("50000")
+    assert port_spec == {'Protocol': 'tcp', 'Port': 50000}

+ 20 - 0
tests/unit/progress_stream_test.py

@@ -65,3 +65,23 @@ class ProgressStreamTestCase(unittest.TestCase):
 
         events = progress_stream.stream_output(events, output)
         self.assertTrue(len(output.getvalue()) > 0)
+
+
+def test_get_digest_from_push():
+    digest = "sha256:abcd"
+    events = [
+        {"status": "..."},
+        {"status": "..."},
+        {"progressDetail": {}, "aux": {"Digest": digest}},
+    ]
+    assert progress_stream.get_digest_from_push(events) == digest
+
+
+def test_get_digest_from_pull():
+    digest = "sha256:abcd"
+    events = [
+        {"status": "..."},
+        {"status": "..."},
+        {"status": "Digest: %s" % digest},
+    ]
+    assert progress_stream.get_digest_from_pull(events) == digest