Merge ~twom/launchpad-buildd:oci-can-be-layer-symlinks into launchpad-buildd:master

Proposed by Tom Wardill
Status: Merged
Approved by: Tom Wardill
Approved revision: 71f6e156728ae16140d91ac71d04958fbb8c170e
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~twom/launchpad-buildd:oci-can-be-layer-symlinks
Merge into: launchpad-buildd:master
Diff against target: 205 lines (+81/-7)
4 files modified
debian/changelog (+6/-0)
lpbuildd/oci.py (+30/-0)
lpbuildd/tests/oci_tarball.py (+31/-6)
lpbuildd/tests/test_oci.py (+14/-1)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Colin Watson (community) Approve
Review via email: mp+401710@code.launchpad.net

Commit message

Handle layer symlinks in OCI builds

Description of the change

Occasionally, docker can produce a saved image tarball that contains symlinks between layers.
This causes the buildd to fail to extract from the image as you can't dereference in a streamed tarfile object.

Instead, keep track of symlinks and then use the paths that are available to copy the existing file to the target place.

Also modify our test created tarball to include an example of this.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

I don't think I really follow this very clearly, but I also don't see any obvious mistakes in it, so :-)

Also don't forget to add an entry to debian/changelog.

review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/debian/changelog b/debian/changelog
index b0372ad..4bc4041 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
1launchpad-buildd (196) UNRELEASED; urgency=medium
2
3 * Handle symlinks in OCI image files
4
5 -- Tom Wardill <tom.wardill@canonical.com> Fri, 23 Apr 2021 15:51:11 +0100
6
1launchpad-buildd (195) bionic; urgency=medium7launchpad-buildd (195) bionic; urgency=medium
28
3 * sbuild-package: Temporarily remove lxd group membership (LP: #1820348).9 * sbuild-package: Temporarily remove lxd group membership (LP: #1820348).
diff --git a/lpbuildd/oci.py b/lpbuildd/oci.py
index cf8d1d7..7a17939 100644
--- a/lpbuildd/oci.py
+++ b/lpbuildd/oci.py
@@ -8,6 +8,7 @@ __metaclass__ = type
8import hashlib8import hashlib
9import json9import json
10import os10import os
11import shutil
11import tarfile12import tarfile
12import tempfile13import tempfile
1314
@@ -168,6 +169,7 @@ class OCIBuildManager(SnapBuildProxyMixin, DebianBuildManager):
168169
169 current_dir = ''170 current_dir = ''
170 directory_tar = None171 directory_tar = None
172 symlinks = []
171 try:173 try:
172 # The tarfile is a stream and must be processed in order174 # The tarfile is a stream and must be processed in order
173 for file in tar:175 for file in tar:
@@ -184,6 +186,16 @@ class OCIBuildManager(SnapBuildProxyMixin, DebianBuildManager):
184 os.path.join(186 os.path.join(
185 extract_path, '{}.tar.gz'.format(file.name)),187 extract_path, '{}.tar.gz'.format(file.name)),
186 'w|gz')188 'w|gz')
189 if file.issym():
190 # symlinks can't be extracted or derefenced from a stream
191 # as you can't seek backwards.
192 # Work out what the symlink is referring to, then
193 # we can deal with it later
194 self._builder.log(
195 "Found symlink at {} referencing {}".format(
196 file.name, file.linkpath))
197 symlinks.append(file)
198 continue
187 if current_dir and file.name.endswith('layer.tar'):199 if current_dir and file.name.endswith('layer.tar'):
188 # This is the actual layer data, we want to add it to200 # This is the actual layer data, we want to add it to
189 # the directory gzip201 # the directory gzip
@@ -203,6 +215,24 @@ class OCIBuildManager(SnapBuildProxyMixin, DebianBuildManager):
203 if directory_tar is not None:215 if directory_tar is not None:
204 directory_tar.close()216 directory_tar.close()
205217
218 # deal with any symlinks we had
219 for symlink in symlinks:
220 # These are paths that finish in "<layer_id>/layer.tar"
221 # we want the directory name, which should always be
222 # the second component
223 source_name = os.path.join(
224 extract_path,
225 "{}.tar.gz".format(symlink.linkpath.split('/')[-2]))
226 target_name = os.path.join(
227 extract_path,
228 '{}.tar.gz'.format(symlink.name.split('/')[-2]))
229 # Do a copy to dereference the symlink
230 self._builder.log(
231 "Deferencing symlink from {} to {}".format(
232 source_name, target_name))
233 shutil.copy(source_name, target_name)
234
235
206 # We need these mapping files236 # We need these mapping files
207 sha_directory = tempfile.mkdtemp()237 sha_directory = tempfile.mkdtemp()
208 # This can change depending on the kernel options / docker package238 # This can change depending on the kernel options / docker package
diff --git a/lpbuildd/tests/oci_tarball.py b/lpbuildd/tests/oci_tarball.py
index 70c055b..21ec013 100644
--- a/lpbuildd/tests/oci_tarball.py
+++ b/lpbuildd/tests/oci_tarball.py
@@ -17,14 +17,18 @@ class OCITarball:
17 @property17 @property
18 def config(self):18 def config(self):
19 return self._makeFile(19 return self._makeFile(
20 {"rootfs": {"diff_ids": ["sha256:diff1", "sha256:diff2"]}},20 {"rootfs": {"diff_ids":
21 ["sha256:diff1", "sha256:diff2", "sha256:diff3"]}},
21 'config.json')22 'config.json')
2223
23 @property24 @property
24 def manifest(self):25 def manifest(self):
25 return self._makeFile(26 return self._makeFile(
26 [{"Config": "config.json",27 [{"Config": "config.json",
27 "Layers": ["layer-1/layer.tar", "layer-2/layer.tar"]}],28 "Layers": [
29 "layer-1/layer.tar",
30 "layer-2/layer.tar",
31 "layer-3/layer.tar"]}],
28 'manifest.json')32 'manifest.json')
2933
30 @property34 @property
@@ -32,16 +36,32 @@ class OCITarball:
32 return self._makeFile([], 'repositories')36 return self._makeFile([], 'repositories')
3337
34 def layer_file(self, directory, layer_name):38 def layer_file(self, directory, layer_name):
39 layer_directory = os.path.join(directory, layer_name)
40 os.mkdir(layer_directory)
35 contents = "{}-contents".format(layer_name)41 contents = "{}-contents".format(layer_name)
36 tarinfo = tarfile.TarInfo(contents)42 tarinfo = tarfile.TarInfo(contents)
37 tarinfo.size = len(contents)43 tarinfo.size = len(contents)
38 layer_contents = io.BytesIO(contents.encode("UTF-8"))44 layer_contents = io.BytesIO(contents.encode("UTF-8"))
39 layer_tar_path = os.path.join(45 layer_tar_path = os.path.join(
40 directory, '{}.tar.gz'.format(layer_name))46 layer_directory, 'layer.tar')
41 layer_tar = tarfile.open(layer_tar_path, 'w:gz')47 layer_tar = tarfile.open(layer_tar_path, 'w')
42 layer_tar.addfile(tarinfo, layer_contents)48 layer_tar.addfile(tarinfo, layer_contents)
43 layer_tar.close()49 layer_tar.close()
44 return layer_tar_path50 return layer_directory
51
52 def add_symlink_layer(self, directory, source_layer, target_layer):
53 target_layer_directory = os.path.join(directory, target_layer)
54 source_layer_directory = os.path.join(directory, source_layer)
55
56 target = os.path.join(target_layer_directory, "layer.tar")
57 source = os.path.join(source_layer_directory, "layer.tar")
58
59 os.mkdir(target_layer_directory)
60 os.symlink(
61 os.path.relpath(source, target_layer_directory),
62 os.path.join(target_layer_directory, "layer.tar")
63 )
64 return target_layer_directory
4565
46 def build_tar_file(self):66 def build_tar_file(self):
47 tar_directory = tempfile.mkdtemp()67 tar_directory = tempfile.mkdtemp()
@@ -53,7 +73,12 @@ class OCITarball:
5373
54 for layer_name in ['layer-1', 'layer-2']:74 for layer_name in ['layer-1', 'layer-2']:
55 layer = self.layer_file(tar_directory, layer_name)75 layer = self.layer_file(tar_directory, layer_name)
56 tar.add(layer, arcname='{}.tar.gz'.format(layer_name))76 tar.add(layer, arcname=layer_name)
77
78 # add a symlink for 'layer-3'
79 target_layer = self.add_symlink_layer(
80 tar_directory, 'layer-2', 'layer-3')
81 tar.add(target_layer, arcname="layer-3")
5782
58 tar.close()83 tar.close()
5984
diff --git a/lpbuildd/tests/test_oci.py b/lpbuildd/tests/test_oci.py
index 15a4d64..78b0dbb 100644
--- a/lpbuildd/tests/test_oci.py
+++ b/lpbuildd/tests/test_oci.py
@@ -43,7 +43,8 @@ class MockBuildManager(OCIBuildManager):
43class MockOCITarSave():43class MockOCITarSave():
44 @property44 @property
45 def stdout(self):45 def stdout(self):
46 return io.open(OCITarball().build_tar_file(), 'rb')46 tar_path = OCITarball().build_tar_file()
47 return io.open(tar_path, 'rb')
4748
4849
49class TestOCIBuildManagerIteration(TestCase):50class TestOCIBuildManagerIteration(TestCase):
@@ -147,6 +148,7 @@ class TestOCIBuildManagerIteration(TestCase):
147 'manifest.json',148 'manifest.json',
148 'layer-1.tar.gz',149 'layer-1.tar.gz',
149 'layer-2.tar.gz',150 'layer-2.tar.gz',
151 'layer-3.tar.gz',
150 'digests.json',152 'digests.json',
151 'config.json',153 'config.json',
152 ]154 ]
@@ -167,6 +169,11 @@ class TestOCIBuildManagerIteration(TestCase):
167 "source": "",169 "source": "",
168 "digest": "testsha",170 "digest": "testsha",
169 "layer_id": "layer-2"171 "layer_id": "layer-2"
172 },
173 "sha256:diff3": {
174 "source": "",
175 "digest": "testsha",
176 "layer_id": "layer-3"
170 }177 }
171 }]178 }]
172 self.assertEqual(digests_expected, json.loads(digests_contents))179 self.assertEqual(digests_expected, json.loads(digests_contents))
@@ -233,6 +240,7 @@ class TestOCIBuildManagerIteration(TestCase):
233 'manifest.json',240 'manifest.json',
234 'layer-1.tar.gz',241 'layer-1.tar.gz',
235 'layer-2.tar.gz',242 'layer-2.tar.gz',
243 'layer-3.tar.gz',
236 'digests.json',244 'digests.json',
237 'config.json',245 'config.json',
238 ]246 ]
@@ -253,6 +261,11 @@ class TestOCIBuildManagerIteration(TestCase):
253 "source": "",261 "source": "",
254 "digest": "testsha",262 "digest": "testsha",
255 "layer_id": "layer-2"263 "layer_id": "layer-2"
264 },
265 "sha256:diff3": {
266 "source": "",
267 "digest": "testsha",
268 "layer_id": "layer-3"
256 }269 }
257 }]270 }]
258 self.assertEqual(digests_expected, json.loads(digests_contents))271 self.assertEqual(digests_expected, json.loads(digests_contents))

Subscribers

People subscribed via source and target branches