1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: Apache-2.0
17 from pathlib import Path
18 from unittest import mock
23 import arvados.commands.keepdocker as arv_keepdocker
24 from . import arvados_testutil as tutil
26 class StopTest(Exception):
30 class ArvKeepdockerTestCase(unittest.TestCase, tutil.VersionChecker):
31 def run_arv_keepdocker(self, args, err, **kwargs):
32 sys.argv = ['arv-keepdocker'] + args
33 log_handler = logging.StreamHandler(err)
34 arv_keepdocker.logger.addHandler(log_handler)
36 return arv_keepdocker.main(**kwargs)
38 arv_keepdocker.logger.removeHandler(log_handler)
40 def test_unsupported_arg(self):
41 out = tutil.StringIO()
42 with tutil.redirected_streams(stdout=out, stderr=out), \
43 self.assertRaises(SystemExit):
44 self.run_arv_keepdocker(['-x=unknown'], sys.stderr)
45 self.assertRegex(out.getvalue(), r'unrecognized arguments')
47 def test_version_argument(self):
48 with tutil.redirected_streams(
49 stdout=tutil.StringIO, stderr=tutil.StringIO) as (out, err):
50 with self.assertRaises(SystemExit):
51 self.run_arv_keepdocker(['--version'], sys.stderr)
52 self.assertVersionOutput(out, err)
54 @mock.patch('arvados.commands.keepdocker.list_images_in_arv',
56 @mock.patch('arvados.commands.keepdocker.find_image_hashes',
57 return_value=['abc123'])
58 @mock.patch('arvados.commands.keepdocker.find_one_image_hash',
59 return_value='abc123')
60 def test_image_format_compatibility(self, _1, _2, _3):
61 old_id = hashlib.sha256(b'old').hexdigest()
62 new_id = 'sha256:'+hashlib.sha256(b'new').hexdigest()
63 for supported, img_id, expect_ok in [
64 (['v1'], old_id, True),
65 (['v1'], new_id, False),
66 (None, old_id, False),
69 (['v1', 'v2'], new_id, True),
70 (['v1'], new_id, False),
71 (['v2'], new_id, True)]:
73 fakeDD = arvados.api('v1')._rootDesc
75 del fakeDD['dockerImageFormats']
77 fakeDD['dockerImageFormats'] = supported
79 err = tutil.StringIO()
80 out = tutil.StringIO()
82 with tutil.redirected_streams(stdout=out), \
83 mock.patch('arvados.api') as api, \
84 mock.patch('arvados.commands.keepdocker.popen_docker',
85 return_value=subprocess.Popen(
87 stdout=subprocess.PIPE)), \
88 mock.patch('arvados.commands.keepdocker.prep_image_file',
89 side_effect=StopTest), \
90 self.assertRaises(StopTest if expect_ok else SystemExit):
92 api()._rootDesc = fakeDD
93 self.run_arv_keepdocker(['--force', 'testimage'], err)
95 self.assertEqual(out.getvalue(), '')
98 err.getvalue(), r"refusing to store",
99 msg=repr((supported, img_id)))
102 err.getvalue(), r"refusing to store",
103 msg=repr((supported, img_id)))
107 r"server does not specify supported image formats",
108 msg=repr((supported, img_id)))
110 fakeDD = arvados.api('v1')._rootDesc
111 fakeDD['dockerImageFormats'] = ['v1']
112 err = tutil.StringIO()
113 out = tutil.StringIO()
114 with tutil.redirected_streams(stdout=out), \
115 mock.patch('arvados.api') as api, \
116 mock.patch('arvados.commands.keepdocker.popen_docker',
117 return_value=subprocess.Popen(
119 stdout=subprocess.PIPE)), \
120 mock.patch('arvados.commands.keepdocker.prep_image_file',
121 side_effect=StopTest), \
122 self.assertRaises(StopTest):
123 api()._rootDesc = fakeDD
124 self.run_arv_keepdocker(
125 ['--force', '--force-image-format', 'testimage'], err)
126 self.assertRegex(err.getvalue(), r"forcing incompatible image")
128 def test_tag_given_twice(self):
129 with tutil.redirected_streams(stdout=tutil.StringIO, stderr=tutil.StringIO) as (out, err):
130 with self.assertRaises(SystemExit):
131 self.run_arv_keepdocker(['myrepo:mytag', 'extratag'], sys.stderr)
132 self.assertRegex(err.getvalue(), r"cannot add tag argument 'extratag'")
134 def test_image_given_as_repo_colon_tag(self):
135 with self.assertRaises(StopTest), \
136 mock.patch('arvados.commands.keepdocker.find_one_image_hash',
137 side_effect=StopTest) as find_image_mock:
138 self.run_arv_keepdocker(['repo:tag'], sys.stderr)
139 find_image_mock.assert_called_with('repo', 'tag')
141 def test_image_given_as_registry_repo_colon_tag(self):
142 with self.assertRaises(StopTest), \
143 mock.patch('arvados.commands.keepdocker.find_one_image_hash',
144 side_effect=StopTest) as find_image_mock:
145 self.run_arv_keepdocker(['myreg.example:8888/repo/img:tag'], sys.stderr)
146 find_image_mock.assert_called_with('myreg.example:8888/repo/img', 'tag')
148 with self.assertRaises(StopTest), \
149 mock.patch('arvados.commands.keepdocker.find_one_image_hash',
150 side_effect=StopTest) as find_image_mock:
151 self.run_arv_keepdocker(['registry.hub.docker.com:443/library/debian:bullseye-slim'], sys.stderr)
152 find_image_mock.assert_called_with('registry.hub.docker.com/library/debian', 'bullseye-slim')
154 def test_image_has_colons(self):
155 with self.assertRaises(StopTest), \
156 mock.patch('arvados.commands.keepdocker.find_one_image_hash',
157 side_effect=StopTest) as find_image_mock:
158 self.run_arv_keepdocker(['[::1]:8888/repo/img'], sys.stderr)
159 find_image_mock.assert_called_with('[::1]:8888/repo/img', 'latest')
161 with self.assertRaises(StopTest), \
162 mock.patch('arvados.commands.keepdocker.find_one_image_hash',
163 side_effect=StopTest) as find_image_mock:
164 self.run_arv_keepdocker(['[::1]/repo/img'], sys.stderr)
165 find_image_mock.assert_called_with('[::1]/repo/img', 'latest')
167 with self.assertRaises(StopTest), \
168 mock.patch('arvados.commands.keepdocker.find_one_image_hash',
169 side_effect=StopTest) as find_image_mock:
170 self.run_arv_keepdocker(['[::1]:8888/repo/img:tag'], sys.stderr)
171 find_image_mock.assert_called_with('[::1]:8888/repo/img', 'tag')
173 def test_list_images_with_host_and_port(self):
174 api = arvados.api('v1')
175 taglink = api.links().create(body={'link': {
176 'link_class': 'docker_image_repo+tag',
177 'name': 'registry.example:1234/repo:latest',
178 'head_uuid': 'zzzzz-4zz18-1v45jub259sjjgb',
181 out = tutil.StringIO()
182 with self.assertRaises(SystemExit):
183 self.run_arv_keepdocker([], sys.stderr, stdout=out)
184 self.assertRegex(out.getvalue(), r'\nregistry.example:1234/repo +latest ')
186 api.links().delete(uuid=taglink['uuid']).execute()
188 @mock.patch('arvados.commands.keepdocker.list_images_in_arv',
190 @mock.patch('arvados.commands.keepdocker.find_image_hashes',
191 return_value=['abc123'])
192 @mock.patch('arvados.commands.keepdocker.find_one_image_hash',
193 return_value='abc123')
194 def test_collection_property_update(self, _1, _2, _3):
195 image_id = 'sha256:'+hashlib.sha256(b'image').hexdigest()
196 fakeDD = arvados.api('v1')._rootDesc
197 fakeDD['dockerImageFormats'] = ['v2']
199 err = tutil.StringIO()
200 out = tutil.StringIO()
201 File = collections.namedtuple('File', ['name'])
202 mocked_file = File(name='docker_image')
203 mocked_collection = {
204 'uuid': 'new-collection-uuid',
206 'responsible_person_uuid': 'person_uuid',
210 with tutil.redirected_streams(stdout=out), \
211 mock.patch('arvados.api') as api, \
212 mock.patch('arvados.commands.keepdocker.popen_docker',
213 return_value=subprocess.Popen(
215 stdout=subprocess.PIPE)), \
216 mock.patch('arvados.commands.keepdocker.prep_image_file',
217 return_value=(mocked_file, False)), \
218 mock.patch('arvados.commands.put.main',
219 return_value='new-collection-uuid'), \
220 self.assertRaises(StopTest):
222 api()._rootDesc = fakeDD
223 api().collections().get().execute.return_value = copy.deepcopy(mocked_collection)
224 api().collections().update().execute.side_effect = StopTest
225 self.run_arv_keepdocker(['--force', 'testimage'], err)
227 updated_properties = mocked_collection['properties']
228 updated_properties.update({'docker-image-repo-tag': 'testimage:latest'})
229 api().collections().update.assert_called_with(
230 uuid=mocked_collection['uuid'],
231 body={'properties': updated_properties})
234 @parameterized.parameterized_class(('filename',), [
235 ('hello-world-ManifestV2.tar',),
236 ('hello-world-ManifestV2-OCILayout.tar',),
238 class ImageMetadataTestCase(unittest.TestCase):
239 DATA_PATH = Path(__file__).parent / 'data'
243 cls.image_file = (cls.DATA_PATH / cls.filename).open('rb')
246 def tearDownClass(cls):
247 cls.image_file.close()
250 self.manifest, self.config = arv_keepdocker.load_image_metadata(self.image_file)
252 def test_image_manifest(self):
253 self.assertIsInstance(self.manifest, collections.abc.Mapping)
254 self.assertEqual(self.manifest.get('RepoTags'), ['hello-world:latest'])
256 def test_image_config(self):
257 self.assertIsInstance(self.config, collections.abc.Mapping)
258 self.assertEqual(self.config.get('created'), '2023-05-02T16:49:27Z')
261 def test_get_cache_dir(tmp_path):
262 actual = arv_keepdocker.get_cache_dir(lambda: tmp_path)
263 assert isinstance(actual, str)
264 actual = Path(actual)
265 assert actual.is_dir()
266 assert actual.name == 'docker'
267 assert actual.parent == tmp_path