Merge lp:~twom/launchpad-buildd/initial-docker-build-support into lp:launchpad-buildd

Proposed by Tom Wardill
Status: Merged
Merged at revision: 413
Proposed branch: lp:~twom/launchpad-buildd/initial-docker-build-support
Merge into: lp:launchpad-buildd
Diff against target: 1478 lines (+1200/-54)
14 files modified
debian/changelog (+10/-4)
lpbuildd/buildd-slave.tac (+2/-0)
lpbuildd/oci.py (+222/-0)
lpbuildd/snap.py (+32/-29)
lpbuildd/target/build_oci.py (+126/-0)
lpbuildd/target/build_snap.py (+7/-15)
lpbuildd/target/cli.py (+2/-0)
lpbuildd/target/lxd.py (+6/-2)
lpbuildd/target/snapbuildproxy.py (+41/-0)
lpbuildd/target/tests/test_build_oci.py (+407/-0)
lpbuildd/target/tests/test_build_snap.py (+1/-1)
lpbuildd/target/tests/test_lxd.py (+4/-3)
lpbuildd/tests/oci_tarball.py (+60/-0)
lpbuildd/tests/test_oci.py (+280/-0)
To merge this branch: bzr merge lp:~twom/launchpad-buildd/initial-docker-build-support
Reviewer Review Type Date Requested Status
Tom Wardill (community) Approve
Colin Watson (community) Approve
Review via email: mp+369775@code.launchpad.net

Commit message

Add initial docker build support

Description of the change

Add a builder for docker, creating an image following a supplied Dockerfile.
Save the image, extract it and then tar each component layer individually for returning/caching in launchpad.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Needs Fixing
393. By Tom Wardill

Use shell_escape, compress output images

394. By Tom Wardill

Collect .tar.gz

395. By Tom Wardill

Use a python script to save docker image to layers

396. By Tom Wardill

Move image extraction to gatherResults

397. By Tom Wardill

Refactor joint proxy handling

398. By Tom Wardill

Fix changelog diff noise

399. By Tom Wardill

Do more preprocessing, create digests file

400. By Tom Wardill

Include all diffs in digests and waiting files

401. By Tom Wardill

Add layer_id to digests now we're using every layer

402. By <email address hidden>

Use docker from apt, rather than from snap

403. By <email address hidden>

Rename Docker to OCI

404. By <email address hidden>

Merge upstream

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
405. By <email address hidden>

Remove conflict marker

406. By <email address hidden>

Use proxy, extract sha from right location

407. By <email address hidden>

Refactor proxy handling into better named mixin

408. By <email address hidden>

Refactor out adding docker proxy

409. By <email address hidden>

Include snap build proxy code

410. By <email address hidden>

Move to an on-the-fly tarball creation

Revision history for this message
Tom Wardill (twom) wrote :

While looking at the launchpad buildbehaviour side of this, realised MP doesn't support build file location.

It should do that.

review: Needs Fixing
411. By <email address hidden>

Argument is build_file, not file

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Tom Wardill (twom) wrote :

Tested with latest buildbehaviour branch, should now be functional.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'debian/changelog'
--- debian/changelog 2020-01-06 15:03:55 +0000
+++ debian/changelog 2020-02-26 10:52:47 +0000
@@ -1,3 +1,9 @@
1launchpad-buildd (187) UNRELEASED; urgency=medium
2
3 * Prototype Docker image building support.
4
5 -- Colin Watson <cjwatson@ubuntu.com> Wed, 05 Jun 2019 15:06:54 +0100
6
1launchpad-buildd (186) xenial; urgency=medium7launchpad-buildd (186) xenial; urgency=medium
28
3 * Fix sbuildrc compatibility with xenial's sbuild.9 * Fix sbuildrc compatibility with xenial's sbuild.
@@ -32,7 +38,7 @@
3238
33 [ Michael Hudson-Doyle ]39 [ Michael Hudson-Doyle ]
34 * Do not make assumptions about what device major number the device mapper40 * Do not make assumptions about what device major number the device mapper
35 is using. (LP: #1852518) 41 is using. (LP: #1852518)
3642
37 -- Colin Watson <cjwatson@ubuntu.com> Tue, 26 Nov 2019 12:22:37 +000043 -- Colin Watson <cjwatson@ubuntu.com> Tue, 26 Nov 2019 12:22:37 +0000
3844
@@ -801,7 +807,7 @@
801 memory at once (LP: #1227086).807 memory at once (LP: #1227086).
802808
803 [ Adam Conrad ]809 [ Adam Conrad ]
804 * Tidy up log formatting of the "Already reaped..." message. 810 * Tidy up log formatting of the "Already reaped..." message.
805811
806 -- Colin Watson <cjwatson@ubuntu.com> Fri, 27 Sep 2013 13:08:59 +0100812 -- Colin Watson <cjwatson@ubuntu.com> Fri, 27 Sep 2013 13:08:59 +0100
807813
@@ -986,7 +992,7 @@
986launchpad-buildd (98) hardy; urgency=low992launchpad-buildd (98) hardy; urgency=low
987993
988 * Add launchpad-buildd dependency on python-apt, as an accomodation for it994 * Add launchpad-buildd dependency on python-apt, as an accomodation for it
989 being only a Recommends but actually required by python-debian. 995 being only a Recommends but actually required by python-debian.
990 LP: #890834996 LP: #890834
991997
992 -- Martin Pool <mbp@canonical.com> Wed, 16 Nov 2011 10:28:48 +1100998 -- Martin Pool <mbp@canonical.com> Wed, 16 Nov 2011 10:28:48 +1100
@@ -1040,7 +1046,7 @@
10401046
1041launchpad-buildd (90) hardy; urgency=low1047launchpad-buildd (90) hardy; urgency=low
10421048
1043 * debhelper is a Build-Depends because it is needed to run 'clean'. 1049 * debhelper is a Build-Depends because it is needed to run 'clean'.
1044 * python-lpbuildd conflicts with launchpad-buildd << 88.1050 * python-lpbuildd conflicts with launchpad-buildd << 88.
1045 * Add and adjust build-arch, binary-arch, build-indep to match policy.1051 * Add and adjust build-arch, binary-arch, build-indep to match policy.
1046 * Complies with stardards version 3.9.2.1052 * Complies with stardards version 3.9.2.
10471053
=== modified file 'lpbuildd/buildd-slave.tac'
--- lpbuildd/buildd-slave.tac 2019-02-12 10:35:12 +0000
+++ lpbuildd/buildd-slave.tac 2020-02-26 10:52:47 +0000
@@ -23,6 +23,7 @@
2323
24from lpbuildd.binarypackage import BinaryPackageBuildManager24from lpbuildd.binarypackage import BinaryPackageBuildManager
25from lpbuildd.builder import XMLRPCBuilder25from lpbuildd.builder import XMLRPCBuilder
26from lpbuildd.oci import OCIBuildManager
26from lpbuildd.livefs import LiveFilesystemBuildManager27from lpbuildd.livefs import LiveFilesystemBuildManager
27from lpbuildd.log import RotatableFileLogObserver28from lpbuildd.log import RotatableFileLogObserver
28from lpbuildd.snap import SnapBuildManager29from lpbuildd.snap import SnapBuildManager
@@ -45,6 +46,7 @@
45 TranslationTemplatesBuildManager, 'translation-templates')46 TranslationTemplatesBuildManager, 'translation-templates')
46builder.registerManager(LiveFilesystemBuildManager, "livefs")47builder.registerManager(LiveFilesystemBuildManager, "livefs")
47builder.registerManager(SnapBuildManager, "snap")48builder.registerManager(SnapBuildManager, "snap")
49builder.registerManager(OCIBuildManager, "oci")
4850
49application = service.Application('Builder')51application = service.Application('Builder')
50application.addComponent(52application.addComponent(
5153
=== added file 'lpbuildd/oci.py'
--- lpbuildd/oci.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/oci.py 2020-02-26 10:52:47 +0000
@@ -0,0 +1,222 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from __future__ import print_function
5
6__metaclass__ = type
7
8import hashlib
9import json
10import os
11import tarfile
12import tempfile
13
14from six.moves.configparser import (
15 NoOptionError,
16 NoSectionError,
17 )
18
19from lpbuildd.debian import (
20 DebianBuildManager,
21 DebianBuildState,
22 )
23from lpbuildd.snap import SnapBuildProxyMixin
24
25
26RETCODE_SUCCESS = 0
27RETCODE_FAILURE_INSTALL = 200
28RETCODE_FAILURE_BUILD = 201
29
30
31class OCIBuildState(DebianBuildState):
32 BUILD_OCI = "BUILD_OCI"
33
34
35class OCIBuildManager(SnapBuildProxyMixin, DebianBuildManager):
36 """Build an OCI Image."""
37
38 backend_name = "lxd"
39 initial_build_state = OCIBuildState.BUILD_OCI
40
41 @property
42 def needs_sanitized_logs(self):
43 return True
44
45 def initiate(self, files, chroot, extra_args):
46 """Initiate a build with a given set of files and chroot."""
47 self.name = extra_args["name"]
48 self.branch = extra_args.get("branch")
49 self.git_repository = extra_args.get("git_repository")
50 self.git_path = extra_args.get("git_path")
51 self.build_file = extra_args.get("build_file")
52 self.proxy_url = extra_args.get("proxy_url")
53 self.revocation_endpoint = extra_args.get("revocation_endpoint")
54 self.proxy_service = None
55
56 super(OCIBuildManager, self).initiate(files, chroot, extra_args)
57
58 def doRunBuild(self):
59 """Run the process to build the snap."""
60 args = []
61 args.extend(self.startProxy())
62 if self.revocation_endpoint:
63 args.extend(["--revocation-endpoint", self.revocation_endpoint])
64 if self.branch is not None:
65 args.extend(["--branch", self.branch])
66 if self.git_repository is not None:
67 args.extend(["--git-repository", self.git_repository])
68 if self.git_path is not None:
69 args.extend(["--git-path", self.git_path])
70 if self.build_file is not None:
71 args.extend(["--build-file", self.build_file])
72 try:
73 snap_store_proxy_url = self._builder._config.get(
74 "proxy", "snapstore")
75 args.extend(["--snap-store-proxy-url", snap_store_proxy_url])
76 except (NoSectionError, NoOptionError):
77 pass
78 args.append(self.name)
79 self.runTargetSubProcess("build-oci", *args)
80
81 def iterate_BUILD_OCI(self, retcode):
82 """Finished building the OCI image."""
83 self.stopProxy()
84 self.revokeProxyToken()
85 if retcode == RETCODE_SUCCESS:
86 print("Returning build status: OK")
87 return self.deferGatherResults()
88 elif (retcode >= RETCODE_FAILURE_INSTALL and
89 retcode <= RETCODE_FAILURE_BUILD):
90 if not self.alreadyfailed:
91 self._builder.buildFail()
92 print("Returning build status: Build failed.")
93 self.alreadyfailed = True
94 else:
95 if not self.alreadyfailed:
96 self._builder.builderFail()
97 print("Returning build status: Builder failed.")
98 self.alreadyfailed = True
99 self.doReapProcesses(self._state)
100
101 def iterateReap_BUILD_OCI(self, retcode):
102 """Finished reaping after building the OCI image."""
103 self._state = DebianBuildState.UMOUNT
104 self.doUnmounting()
105
106 def _calculateLayerSha(self, layer_path):
107 with open(layer_path, 'rb') as layer_tar:
108 sha256_hash = hashlib.sha256()
109 for byte_block in iter(lambda: layer_tar.read(4096), b""):
110 sha256_hash.update(byte_block)
111 digest = sha256_hash.hexdigest()
112 return digest
113
114 def _gatherManifestSection(self, section, extract_path, sha_directory):
115 config_file_path = os.path.join(extract_path, section["Config"])
116 self._builder.addWaitingFile(config_file_path)
117 with open(config_file_path, 'r') as config_fp:
118 config = json.load(config_fp)
119 diff_ids = config["rootfs"]["diff_ids"]
120 digest_diff_map = {}
121 for diff_id, layer_id in zip(diff_ids, section['Layers']):
122 layer_id = layer_id.split('/')[0]
123 diff_file = os.path.join(sha_directory, diff_id.split(':')[1])
124 layer_path = os.path.join(
125 extract_path, "{}.tar.gz".format(layer_id))
126 self._builder.addWaitingFile(layer_path)
127 # If we have a mapping between diff and existing digest,
128 # this means this layer has been pulled from a remote.
129 # We should maintain the same digest to achieve layer reuse
130 if os.path.exists(diff_file):
131 with open(diff_file, 'r') as diff_fp:
132 diff = json.load(diff_fp)
133 # We should be able to just take the first occurence,
134 # as that will be the 'most parent' image
135 digest = diff[0]["Digest"]
136 source = diff[0]["SourceRepository"]
137 # If the layer has been build locally, we need to generate the
138 # digest and then set the source to empty
139 else:
140 source = ""
141 digest = self._calculateLayerSha(layer_path)
142 digest_diff_map[diff_id] = {
143 "digest": digest,
144 "source": source,
145 "layer_id": layer_id
146 }
147
148 return digest_diff_map
149
150 def gatherResults(self):
151 """Gather the results of the build and add them to the file cache."""
152 extract_path = tempfile.mkdtemp(prefix=self.name)
153 proc = self.backend.run(
154 ['docker', 'save', self.name],
155 get_output=True, universal_newlines=False, return_process=True)
156 try:
157 tar = tarfile.open(fileobj=proc.stdout, mode="r|")
158 except Exception as e:
159 print(e)
160
161 current_dir = ''
162 directory_tar = None
163 try:
164 # The tarfile is a stream and must be processed in order
165 for file in tar:
166 # Directories are just nodes, you can't extract the children
167 # directly, so keep track of what dir we're in.
168 if file.isdir():
169 current_dir = file.name
170 if directory_tar:
171 # Close the old directory if we have one
172 directory_tar.close()
173 # We're going to add the layer.tar to a gzip
174 directory_tar = tarfile.open(
175 os.path.join(
176 extract_path, '{}.tar.gz'.format(file.name)),
177 'w|gz')
178 if current_dir and file.name.endswith('layer.tar'):
179 # This is the actual layer data, we want to add it to
180 # the directory gzip
181 file.name = file.name.split('/')[1]
182 directory_tar.addfile(file, tar.extractfile(file))
183 elif current_dir and file.name.startswith(current_dir):
184 # Other files that are in the layer directories,
185 # we don't care about
186 continue
187 else:
188 # If it's not in a directory, we need that
189 tar.extract(file, extract_path)
190 except Exception as e:
191 print(e)
192
193 # We need these mapping files
194 sha_directory = tempfile.mkdtemp()
195 sha_path = ('/var/lib/docker/image/'
196 'vfs/distribution/v2metadata-by-diffid/sha256/')
197 sha_files = [x for x in self.backend.listdir(sha_path)
198 if not x.startswith('.')]
199 for file in sha_files:
200 self.backend.copy_out(
201 os.path.join(sha_path, file),
202 os.path.join(sha_directory, file)
203 )
204
205 # Parse the manifest for the other files we need
206 manifest_path = os.path.join(extract_path, 'manifest.json')
207 self._builder.addWaitingFile(manifest_path)
208 with open(manifest_path) as manifest_fp:
209 manifest = json.load(manifest_fp)
210
211 digest_maps = []
212 try:
213 for section in manifest:
214 digest_maps.append(
215 self._gatherManifestSection(section, extract_path,
216 sha_directory))
217 digest_map_file = os.path.join(extract_path, 'digests.json')
218 with open(digest_map_file, 'w') as digest_map_fp:
219 json.dump(digest_maps, digest_map_fp)
220 self._builder.addWaitingFile(digest_map_file)
221 except Exception as e:
222 print(e)
0223
=== modified file 'lpbuildd/snap.py'
--- lpbuildd/snap.py 2019-10-30 12:31:53 +0000
+++ lpbuildd/snap.py 2020-02-26 10:52:47 +0000
@@ -239,35 +239,7 @@
239 BUILD_SNAP = "BUILD_SNAP"239 BUILD_SNAP = "BUILD_SNAP"
240240
241241
242class SnapBuildManager(DebianBuildManager):242class SnapBuildProxyMixin():
243 """Build a snap."""
244
245 backend_name = "lxd"
246 initial_build_state = SnapBuildState.BUILD_SNAP
247
248 @property
249 def needs_sanitized_logs(self):
250 return True
251
252 def initiate(self, files, chroot, extra_args):
253 """Initiate a build with a given set of files and chroot."""
254 self.name = extra_args["name"]
255 self.channels = extra_args.get("channels", {})
256 self.build_request_id = extra_args.get("build_request_id")
257 self.build_request_timestamp = extra_args.get(
258 "build_request_timestamp")
259 self.build_url = extra_args.get("build_url")
260 self.branch = extra_args.get("branch")
261 self.git_repository = extra_args.get("git_repository")
262 self.git_path = extra_args.get("git_path")
263 self.proxy_url = extra_args.get("proxy_url")
264 self.revocation_endpoint = extra_args.get("revocation_endpoint")
265 self.build_source_tarball = extra_args.get(
266 "build_source_tarball", False)
267 self.private = extra_args.get("private", False)
268 self.proxy_service = None
269
270 super(SnapBuildManager, self).initiate(files, chroot, extra_args)
271243
272 def startProxy(self):244 def startProxy(self):
273 """Start the local snap proxy, if necessary."""245 """Start the local snap proxy, if necessary."""
@@ -308,6 +280,37 @@
308 self._builder.log(280 self._builder.log(
309 "Unable to revoke token for %s: %s" % (url.username, e))281 "Unable to revoke token for %s: %s" % (url.username, e))
310282
283
284class SnapBuildManager(SnapBuildProxyMixin, DebianBuildManager):
285 """Build a snap."""
286
287 backend_name = "lxd"
288 initial_build_state = SnapBuildState.BUILD_SNAP
289
290 @property
291 def needs_sanitized_logs(self):
292 return True
293
294 def initiate(self, files, chroot, extra_args):
295 """Initiate a build with a given set of files and chroot."""
296 self.name = extra_args["name"]
297 self.channels = extra_args.get("channels", {})
298 self.build_request_id = extra_args.get("build_request_id")
299 self.build_request_timestamp = extra_args.get(
300 "build_request_timestamp")
301 self.build_url = extra_args.get("build_url")
302 self.branch = extra_args.get("branch")
303 self.git_repository = extra_args.get("git_repository")
304 self.git_path = extra_args.get("git_path")
305 self.proxy_url = extra_args.get("proxy_url")
306 self.revocation_endpoint = extra_args.get("revocation_endpoint")
307 self.build_source_tarball = extra_args.get(
308 "build_source_tarball", False)
309 self.private = extra_args.get("private", False)
310 self.proxy_service = None
311
312 super(SnapBuildManager, self).initiate(files, chroot, extra_args)
313
311 def status(self):314 def status(self):
312 status_path = get_build_path(self.home, self._buildid, "status")315 status_path = get_build_path(self.home, self._buildid, "status")
313 try:316 try:
314317
=== added file 'lpbuildd/target/build_oci.py'
--- lpbuildd/target/build_oci.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/target/build_oci.py 2020-02-26 10:52:47 +0000
@@ -0,0 +1,126 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from __future__ import print_function
5
6__metaclass__ = type
7
8from collections import OrderedDict
9import logging
10import os.path
11import sys
12import tempfile
13from textwrap import dedent
14
15from lpbuildd.target.operation import Operation
16from lpbuildd.target.snapbuildproxy import SnapBuildProxyOperationMixin
17from lpbuildd.target.snapstore import SnapStoreOperationMixin
18from lpbuildd.target.vcs import VCSOperationMixin
19
20
21RETCODE_FAILURE_INSTALL = 200
22RETCODE_FAILURE_BUILD = 201
23
24
25logger = logging.getLogger(__name__)
26
27
28class BuildOCI(SnapBuildProxyOperationMixin, VCSOperationMixin,
29 SnapStoreOperationMixin, Operation):
30
31 description = "Build an OCI image."
32
33 @classmethod
34 def add_arguments(cls, parser):
35 super(BuildOCI, cls).add_arguments(parser)
36 parser.add_argument(
37 "--build-file", help="path to Dockerfile in branch")
38 parser.add_argument("name", help="name of snap to build")
39
40 def __init__(self, args, parser):
41 super(BuildOCI, self).__init__(args, parser)
42 self.bin = os.path.dirname(sys.argv[0])
43
44 def _add_docker_engine_proxy_settings(self):
45 """Add systemd file for docker proxy settings."""
46 # Create containing directory for systemd overrides
47 self.backend.run(
48 ["mkdir", "-p", "/etc/systemd/system/docker.service.d"])
49 # we need both http_proxy and https_proxy. The contents of the files
50 # are otherwise identical
51 for setting in ['http_proxy', 'https_proxy']:
52 contents = dedent("""[Service]
53 Environment="{}={}"
54 """.format(setting.upper(), self.args.proxy_url))
55 file_path = "/etc/systemd/system/docker.service.d/{}.conf".format(
56 setting)
57 with tempfile.NamedTemporaryFile(mode="w+") as systemd_file:
58 systemd_file.write(contents)
59 systemd_file.flush()
60 self.backend.copy_in(systemd_file.name, file_path)
61
62 def run_build_command(self, args, env=None, **kwargs):
63 """Run a build command in the target.
64
65 :param args: the command and arguments to run.
66 :param env: dictionary of additional environment variables to set.
67 :param kwargs: any other keyword arguments to pass to Backend.run.
68 """
69 full_env = OrderedDict()
70 full_env["LANG"] = "C.UTF-8"
71 full_env["SHELL"] = "/bin/sh"
72 if env:
73 full_env.update(env)
74 return self.backend.run(args, env=full_env, **kwargs)
75
76 def install(self):
77 logger.info("Running install phase...")
78 deps = []
79 if self.args.proxy_url:
80 deps.extend(self.proxy_deps)
81 self.install_git_proxy()
82 # Add any proxy settings that are needed
83 self._add_docker_engine_proxy_settings()
84 deps.extend(self.vcs_deps)
85 deps.extend(["docker.io"])
86 self.backend.run(["apt-get", "-y", "install"] + deps)
87 if self.args.backend in ("lxd", "fake"):
88 self.snap_store_set_proxy()
89 self.backend.run(["systemctl", "restart", "docker"])
90 # The docker snap can't see /build, so we have to do our work under
91 # /home/buildd instead. Make sure it exists.
92 self.backend.run(["mkdir", "-p", "/home/buildd"])
93
94 def repo(self):
95 """Collect git or bzr branch."""
96 logger.info("Running repo phase...")
97 env = self.build_proxy_environment(proxy_url=self.args.proxy_url)
98 self.vcs_fetch(self.args.name, cwd="/home/buildd", env=env)
99
100 def build(self):
101 logger.info("Running build phase...")
102 args = ["docker", "build", "--no-cache"]
103 if self.args.proxy_url:
104 for var in ("http_proxy", "https_proxy"):
105 args.extend(
106 ["--build-arg", "{}={}".format(var, self.args.proxy_url)])
107 args.extend(["--tag", self.args.name])
108 if self.args.build_file is not None:
109 args.extend(["--file", self.args.build_file])
110 buildd_path = os.path.join("/home/buildd", self.args.name)
111 args.append(buildd_path)
112 self.run_build_command(args)
113
114 def run(self):
115 try:
116 self.install()
117 except Exception:
118 logger.exception('Install failed')
119 return RETCODE_FAILURE_INSTALL
120 try:
121 self.repo()
122 self.build()
123 except Exception:
124 logger.exception('Build failed')
125 return RETCODE_FAILURE_BUILD
126 return 0
0127
=== modified file 'lpbuildd/target/build_snap.py'
--- lpbuildd/target/build_snap.py 2019-10-30 12:31:53 +0000
+++ lpbuildd/target/build_snap.py 2020-02-26 10:52:47 +0000
@@ -17,6 +17,7 @@
17from six.moves.urllib.parse import urlparse17from six.moves.urllib.parse import urlparse
1818
19from lpbuildd.target.operation import Operation19from lpbuildd.target.operation import Operation
20from lpbuildd.target.snapbuildproxy import SnapBuildProxyOperationMixin
20from lpbuildd.target.snapstore import SnapStoreOperationMixin21from lpbuildd.target.snapstore import SnapStoreOperationMixin
21from lpbuildd.target.vcs import VCSOperationMixin22from lpbuildd.target.vcs import VCSOperationMixin
2223
@@ -46,7 +47,8 @@
46 getattr(namespace, self.dest)[snap] = channel47 getattr(namespace, self.dest)[snap] = channel
4748
4849
49class BuildSnap(VCSOperationMixin, SnapStoreOperationMixin, Operation):50class BuildSnap(SnapBuildProxyOperationMixin, VCSOperationMixin,
51 SnapStoreOperationMixin, Operation):
5052
51 description = "Build a snap."53 description = "Build a snap."
5254
@@ -69,10 +71,6 @@
69 help="RFC3339 timestamp of the Launchpad build request")71 help="RFC3339 timestamp of the Launchpad build request")
70 parser.add_argument(72 parser.add_argument(
71 "--build-url", help="URL of this build on Launchpad")73 "--build-url", help="URL of this build on Launchpad")
72 parser.add_argument("--proxy-url", help="builder proxy url")
73 parser.add_argument(
74 "--revocation-endpoint",
75 help="builder proxy token revocation endpoint")
76 parser.add_argument(74 parser.add_argument(
77 "--build-source-tarball", default=False, action="store_true",75 "--build-source-tarball", default=False, action="store_true",
78 help=(76 help=(
@@ -137,6 +135,9 @@
137 def install(self):135 def install(self):
138 logger.info("Running install phase...")136 logger.info("Running install phase...")
139 deps = []137 deps = []
138 if self.args.proxy_url:
139 deps.extend(self.proxy_deps)
140 self.install_git_proxy()
140 if self.args.backend == "lxd":141 if self.args.backend == "lxd":
141 # udev is installed explicitly to work around142 # udev is installed explicitly to work around
142 # https://bugs.launchpad.net/snapd/+bug/1731519.143 # https://bugs.launchpad.net/snapd/+bug/1731519.
@@ -144,8 +145,6 @@
144 if self.backend.is_package_available(dep):145 if self.backend.is_package_available(dep):
145 deps.append(dep)146 deps.append(dep)
146 deps.extend(self.vcs_deps)147 deps.extend(self.vcs_deps)
147 if self.args.proxy_url:
148 deps.extend(["python3", "socat"])
149 if "snapcraft" in self.args.channels:148 if "snapcraft" in self.args.channels:
150 # snapcraft requires sudo in lots of places, but can't depend on149 # snapcraft requires sudo in lots of places, but can't depend on
151 # it when installed as a snap.150 # it when installed as a snap.
@@ -167,19 +166,12 @@
167 "--channel=%s" % self.args.channels["snapcraft"],166 "--channel=%s" % self.args.channels["snapcraft"],
168 "snapcraft"])167 "snapcraft"])
169 if self.args.proxy_url:168 if self.args.proxy_url:
170 self.backend.copy_in(
171 os.path.join(self.bin, "snap-git-proxy"),
172 "/usr/local/bin/snap-git-proxy")
173 self.install_svn_servers()169 self.install_svn_servers()
174170
175 def repo(self):171 def repo(self):
176 """Collect git or bzr branch."""172 """Collect git or bzr branch."""
177 logger.info("Running repo phase...")173 logger.info("Running repo phase...")
178 env = OrderedDict()174 env = self.build_proxy_environment(proxy_url=self.args.proxy_url)
179 if self.args.proxy_url:
180 env["http_proxy"] = self.args.proxy_url
181 env["https_proxy"] = self.args.proxy_url
182 env["GIT_PROXY_COMMAND"] = "/usr/local/bin/snap-git-proxy"
183 self.vcs_fetch(self.args.name, cwd="/build", env=env)175 self.vcs_fetch(self.args.name, cwd="/build", env=env)
184 status = {}176 status = {}
185 if self.args.branch is not None:177 if self.args.branch is not None:
186178
=== modified file 'lpbuildd/target/cli.py'
--- lpbuildd/target/cli.py 2017-09-08 15:57:18 +0000
+++ lpbuildd/target/cli.py 2020-02-26 10:52:47 +0000
@@ -14,6 +14,7 @@
14 OverrideSourcesList,14 OverrideSourcesList,
15 Update,15 Update,
16 )16 )
17from lpbuildd.target.build_oci import BuildOCI
17from lpbuildd.target.build_livefs import BuildLiveFS18from lpbuildd.target.build_livefs import BuildLiveFS
18from lpbuildd.target.build_snap import BuildSnap19from lpbuildd.target.build_snap import BuildSnap
19from lpbuildd.target.generate_translation_templates import (20from lpbuildd.target.generate_translation_templates import (
@@ -49,6 +50,7 @@
4950
50operations = {51operations = {
51 "add-trusted-keys": AddTrustedKeys,52 "add-trusted-keys": AddTrustedKeys,
53 "build-oci": BuildOCI,
52 "buildlivefs": BuildLiveFS,54 "buildlivefs": BuildLiveFS,
53 "buildsnap": BuildSnap,55 "buildsnap": BuildSnap,
54 "generate-translation-templates": GenerateTranslationTemplates,56 "generate-translation-templates": GenerateTranslationTemplates,
5557
=== modified file 'lpbuildd/target/lxd.py'
--- lpbuildd/target/lxd.py 2019-12-09 11:17:14 +0000
+++ lpbuildd/target/lxd.py 2020-02-26 10:52:47 +0000
@@ -504,7 +504,8 @@
504 "/etc/systemd/system/snapd.refresh.timer"])504 "/etc/systemd/system/snapd.refresh.timer"])
505505
506 def run(self, args, cwd=None, env=None, input_text=None, get_output=False,506 def run(self, args, cwd=None, env=None, input_text=None, get_output=False,
507 echo=False, **kwargs):507 echo=False, return_process=False, universal_newlines=True,
508 **kwargs):
508 """See `Backend`."""509 """See `Backend`."""
509 env_params = []510 env_params = []
510 if env:511 if env:
@@ -538,7 +539,10 @@
538 if get_output:539 if get_output:
539 kwargs["stdout"] = subprocess.PIPE540 kwargs["stdout"] = subprocess.PIPE
540 proc = subprocess.Popen(541 proc = subprocess.Popen(
541 cmd, stdin=subprocess.PIPE, universal_newlines=True, **kwargs)542 cmd, stdin=subprocess.PIPE,
543 universal_newlines=universal_newlines, **kwargs)
544 if return_process:
545 return proc
542 output, _ = proc.communicate(input_text)546 output, _ = proc.communicate(input_text)
543 if proc.returncode:547 if proc.returncode:
544 raise subprocess.CalledProcessError(proc.returncode, cmd)548 raise subprocess.CalledProcessError(proc.returncode, cmd)
545549
=== added file 'lpbuildd/target/snapbuildproxy.py'
--- lpbuildd/target/snapbuildproxy.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/target/snapbuildproxy.py 2020-02-26 10:52:47 +0000
@@ -0,0 +1,41 @@
1# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from __future__ import print_function
5
6__metaclass__ = type
7
8from collections import OrderedDict
9import os
10
11
12class SnapBuildProxyOperationMixin:
13 """Methods supporting the build time HTTP proxy for snap and OCI builds."""
14
15 @classmethod
16 def add_arguments(cls, parser):
17 super(SnapBuildProxyOperationMixin, cls).add_arguments(parser)
18 parser.add_argument("--proxy-url", help="builder proxy url")
19 parser.add_argument(
20 "--revocation-endpoint",
21 help="builder proxy token revocation endpoint")
22
23 @property
24 def proxy_deps(self):
25 return ["python3", "socat"]
26
27 def install_git_proxy(self):
28 self.backend.copy_in(
29 os.path.join(self.bin, "snap-git-proxy"),
30 "/usr/local/bin/snap-git-proxy")
31
32 def build_proxy_environment(self, proxy_url=None, env=None):
33 """Extend a command environment to include http proxy variables."""
34 full_env = OrderedDict()
35 if env:
36 full_env.update(env)
37 if proxy_url:
38 full_env["http_proxy"] = self.args.proxy_url
39 full_env["https_proxy"] = self.args.proxy_url
40 full_env["GIT_PROXY_COMMAND"] = "/usr/local/bin/snap-git-proxy"
41 return full_env
042
=== added file 'lpbuildd/target/tests/test_build_oci.py'
--- lpbuildd/target/tests/test_build_oci.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/target/tests/test_build_oci.py 2020-02-26 10:52:47 +0000
@@ -0,0 +1,407 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6import os.path
7import stat
8import subprocess
9from textwrap import dedent
10
11from fixtures import (
12 FakeLogger,
13 TempDir,
14 )
15import responses
16from systemfixtures import FakeFilesystem
17from testtools import TestCase
18from testtools.matchers import (
19 AnyMatch,
20 Equals,
21 Is,
22 MatchesAll,
23 MatchesDict,
24 MatchesListwise,
25 )
26
27from lpbuildd.target.build_oci import (
28 RETCODE_FAILURE_BUILD,
29 RETCODE_FAILURE_INSTALL,
30 )
31from lpbuildd.target.cli import parse_args
32from lpbuildd.tests.fakebuilder import FakeMethod
33
34
35class RanCommand(MatchesListwise):
36
37 def __init__(self, args, echo=None, cwd=None, input_text=None,
38 get_output=None, **env):
39 kwargs_matcher = {}
40 if echo is not None:
41 kwargs_matcher["echo"] = Is(echo)
42 if cwd:
43 kwargs_matcher["cwd"] = Equals(cwd)
44 if input_text:
45 kwargs_matcher["input_text"] = Equals(input_text)
46 if get_output is not None:
47 kwargs_matcher["get_output"] = Is(get_output)
48 if env:
49 kwargs_matcher["env"] = MatchesDict(
50 {key: Equals(value) for key, value in env.items()})
51 super(RanCommand, self).__init__(
52 [Equals((args,)), MatchesDict(kwargs_matcher)])
53
54
55class RanAptGet(RanCommand):
56
57 def __init__(self, *args):
58 super(RanAptGet, self).__init__(["apt-get", "-y"] + list(args))
59
60
61class RanSnap(RanCommand):
62
63 def __init__(self, *args, **kwargs):
64 super(RanSnap, self).__init__(["snap"] + list(args), **kwargs)
65
66
67class RanBuildCommand(RanCommand):
68
69 def __init__(self, args, **kwargs):
70 kwargs.setdefault("LANG", "C.UTF-8")
71 kwargs.setdefault("SHELL", "/bin/sh")
72 super(RanBuildCommand, self).__init__(args, **kwargs)
73
74
75class TestBuildOCI(TestCase):
76
77 def test_run_build_command_no_env(self):
78 args = [
79 "build-oci",
80 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
81 "--branch", "lp:foo", "test-image",
82 ]
83 build_oci = parse_args(args=args).operation
84 build_oci.run_build_command(["echo", "hello world"])
85 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
86 RanBuildCommand(["echo", "hello world"]),
87 ]))
88
89 def test_run_build_command_env(self):
90 args = [
91 "build-oci",
92 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
93 "--branch", "lp:foo", "test-image",
94 ]
95 build_oci = parse_args(args=args).operation
96 build_oci.run_build_command(
97 ["echo", "hello world"], env={"FOO": "bar baz"})
98 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
99 RanBuildCommand(["echo", "hello world"], FOO="bar baz"),
100 ]))
101
102 def test_install_bzr(self):
103 args = [
104 "build-oci",
105 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
106 "--branch", "lp:foo", "test-image"
107 ]
108 build_oci = parse_args(args=args).operation
109 build_oci.install()
110 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
111 RanAptGet("install", "bzr", "docker.io"),
112 RanCommand(["systemctl", "restart", "docker"]),
113 RanCommand(["mkdir", "-p", "/home/buildd"]),
114 ]))
115
116 def test_install_git(self):
117 args = [
118 "build-oci",
119 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
120 "--git-repository", "lp:foo", "test-image"
121 ]
122 build_oci = parse_args(args=args).operation
123 build_oci.install()
124 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
125 RanAptGet("install", "git", "docker.io"),
126 RanCommand(["systemctl", "restart", "docker"]),
127 RanCommand(["mkdir", "-p", "/home/buildd"]),
128 ]))
129
130 @responses.activate
131 def test_install_snap_store_proxy(self):
132 store_assertion = dedent("""\
133 type: store
134 store: store-id
135 url: http://snap-store-proxy.example
136
137 body
138 """)
139
140 def respond(request):
141 return 200, {"X-Assertion-Store-Id": "store-id"}, store_assertion
142
143 responses.add_callback(
144 "GET", "http://snap-store-proxy.example/v2/auth/store/assertions",
145 callback=respond)
146 args = [
147 "buildsnap",
148 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
149 "--git-repository", "lp:foo",
150 "--snap-store-proxy-url", "http://snap-store-proxy.example/",
151 "test-snap",
152 ]
153 build_snap = parse_args(args=args).operation
154 build_snap.install()
155 self.assertThat(build_snap.backend.run.calls, MatchesListwise([
156 RanAptGet("install", "git", "snapcraft"),
157 RanCommand(
158 ["snap", "ack", "/dev/stdin"], input_text=store_assertion),
159 RanCommand(["snap", "set", "core", "proxy.store=store-id"]),
160 ]))
161
162 def test_install_proxy(self):
163 args = [
164 "build-oci",
165 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
166 "--git-repository", "lp:foo",
167 "--proxy-url", "http://proxy.example:3128/",
168 "test-image",
169 ]
170 build_oci = parse_args(args=args).operation
171 build_oci.bin = "/builderbin"
172 self.useFixture(FakeFilesystem()).add("/builderbin")
173 os.mkdir("/builderbin")
174 with open("/builderbin/snap-git-proxy", "w") as proxy_script:
175 proxy_script.write("proxy script\n")
176 os.fchmod(proxy_script.fileno(), 0o755)
177 build_oci.install()
178 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
179 RanCommand(
180 ["mkdir", "-p", "/etc/systemd/system/docker.service.d"]),
181 RanAptGet("install", "python3", "socat", "git", "docker.io"),
182 RanCommand(["systemctl", "restart", "docker"]),
183 RanCommand(["mkdir", "-p", "/home/buildd"]),
184 ]))
185 self.assertEqual(
186 (b"proxy script\n", stat.S_IFREG | 0o755),
187 build_oci.backend.backend_fs["/usr/local/bin/snap-git-proxy"])
188
189 def test_repo_bzr(self):
190 args = [
191 "build-oci",
192 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
193 "--branch", "lp:foo", "test-image",
194 ]
195 build_oci = parse_args(args=args).operation
196 build_oci.backend.build_path = self.useFixture(TempDir()).path
197 build_oci.backend.run = FakeMethod()
198 build_oci.repo()
199 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
200 RanBuildCommand(
201 ["bzr", "branch", "lp:foo", "test-image"], cwd="/home/buildd"),
202 ]))
203
204 def test_repo_git(self):
205 args = [
206 "build-oci",
207 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
208 "--git-repository", "lp:foo", "test-image",
209 ]
210 build_oci = parse_args(args=args).operation
211 build_oci.backend.build_path = self.useFixture(TempDir()).path
212 build_oci.backend.run = FakeMethod()
213 build_oci.repo()
214 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
215 RanBuildCommand(
216 ["git", "clone", "lp:foo", "test-image"], cwd="/home/buildd"),
217 RanBuildCommand(
218 ["git", "submodule", "update", "--init", "--recursive"],
219 cwd="/home/buildd/test-image"),
220 ]))
221
222 def test_repo_git_with_path(self):
223 args = [
224 "build-oci",
225 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
226 "--git-repository", "lp:foo", "--git-path", "next", "test-image",
227 ]
228 build_oci = parse_args(args=args).operation
229 build_oci.backend.build_path = self.useFixture(TempDir()).path
230 build_oci.backend.run = FakeMethod()
231 build_oci.repo()
232 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
233 RanBuildCommand(
234 ["git", "clone", "-b", "next", "lp:foo", "test-image"],
235 cwd="/home/buildd"),
236 RanBuildCommand(
237 ["git", "submodule", "update", "--init", "--recursive"],
238 cwd="/home/buildd/test-image"),
239 ]))
240
241 def test_repo_git_with_tag_path(self):
242 args = [
243 "build-oci",
244 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
245 "--git-repository", "lp:foo", "--git-path", "refs/tags/1.0",
246 "test-image",
247 ]
248 build_oci = parse_args(args=args).operation
249 build_oci.backend.build_path = self.useFixture(TempDir()).path
250 build_oci.backend.run = FakeMethod()
251 build_oci.repo()
252 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
253 RanBuildCommand(
254 ["git", "clone", "-b", "1.0", "lp:foo", "test-image"],
255 cwd="/home/buildd"),
256 RanBuildCommand(
257 ["git", "submodule", "update", "--init", "--recursive"],
258 cwd="/home/buildd/test-image"),
259 ]))
260
261 def test_repo_proxy(self):
262 args = [
263 "build-oci",
264 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
265 "--git-repository", "lp:foo",
266 "--proxy-url", "http://proxy.example:3128/",
267 "test-image",
268 ]
269 build_oci = parse_args(args=args).operation
270 build_oci.backend.build_path = self.useFixture(TempDir()).path
271 build_oci.backend.run = FakeMethod()
272 build_oci.repo()
273 env = {
274 "http_proxy": "http://proxy.example:3128/",
275 "https_proxy": "http://proxy.example:3128/",
276 "GIT_PROXY_COMMAND": "/usr/local/bin/snap-git-proxy",
277 }
278 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
279 RanBuildCommand(
280 ["git", "clone", "lp:foo", "test-image"],
281 cwd="/home/buildd", **env),
282 RanBuildCommand(
283 ["git", "submodule", "update", "--init", "--recursive"],
284 cwd="/home/buildd/test-image", **env),
285 ]))
286
287 def test_build(self):
288 args = [
289 "build-oci",
290 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
291 "--branch", "lp:foo", "test-image",
292 ]
293 build_oci = parse_args(args=args).operation
294 build_oci.backend.add_dir('/build/test-directory')
295 build_oci.build()
296 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
297 RanBuildCommand(
298 ["docker", "build", "--no-cache", "--tag", "test-image",
299 "/home/buildd/test-image"]),
300 ]))
301
302 def test_build_with_file(self):
303 args = [
304 "build-oci",
305 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
306 "--branch", "lp:foo", "--build-file", "build-aux/Dockerfile",
307 "test-image",
308 ]
309 build_oci = parse_args(args=args).operation
310 build_oci.backend.add_dir('/build/test-directory')
311 build_oci.build()
312 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
313 RanBuildCommand(
314 ["docker", "build", "--no-cache", "--tag", "test-image",
315 "--file", "build-aux/Dockerfile",
316 "/home/buildd/test-image"]),
317 ]))
318
319 def test_build_proxy(self):
320 args = [
321 "build-oci",
322 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
323 "--branch", "lp:foo", "--proxy-url", "http://proxy.example:3128/",
324 "test-image",
325 ]
326 build_oci = parse_args(args=args).operation
327 build_oci.backend.add_dir('/build/test-directory')
328 build_oci.build()
329 self.assertThat(build_oci.backend.run.calls, MatchesListwise([
330 RanBuildCommand(
331 ["docker", "build", "--no-cache",
332 "--build-arg", "http_proxy=http://proxy.example:3128/",
333 "--build-arg", "https_proxy=http://proxy.example:3128/",
334 "--tag", "test-image", "/home/buildd/test-image"]),
335 ]))
336
337 def test_run_succeeds(self):
338 args = [
339 "build-oci",
340 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
341 "--branch", "lp:foo", "test-image",
342 ]
343 build_oci = parse_args(args=args).operation
344 build_oci.backend.build_path = self.useFixture(TempDir()).path
345 build_oci.backend.run = FakeMethod()
346 self.assertEqual(0, build_oci.run())
347 self.assertThat(build_oci.backend.run.calls, MatchesAll(
348 AnyMatch(RanAptGet("install", "bzr", "docker.io")),
349 AnyMatch(RanBuildCommand(
350 ["bzr", "branch", "lp:foo", "test-image"],
351 cwd="/home/buildd")),
352 AnyMatch(RanBuildCommand(
353 ["docker", "build", "--no-cache", "--tag", "test-image",
354 "/home/buildd/test-image"])),
355 ))
356
357 def test_run_install_fails(self):
358 class FailInstall(FakeMethod):
359 def __call__(self, run_args, *args, **kwargs):
360 super(FailInstall, self).__call__(run_args, *args, **kwargs)
361 if run_args[0] == "apt-get":
362 raise subprocess.CalledProcessError(1, run_args)
363
364 self.useFixture(FakeLogger())
365 args = [
366 "build-oci",
367 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
368 "--branch", "lp:foo", "test-image",
369 ]
370 build_oci = parse_args(args=args).operation
371 build_oci.backend.run = FailInstall()
372 self.assertEqual(RETCODE_FAILURE_INSTALL, build_oci.run())
373
374 def test_run_repo_fails(self):
375 class FailRepo(FakeMethod):
376 def __call__(self, run_args, *args, **kwargs):
377 super(FailRepo, self).__call__(run_args, *args, **kwargs)
378 if run_args[:2] == ["bzr", "branch"]:
379 raise subprocess.CalledProcessError(1, run_args)
380
381 self.useFixture(FakeLogger())
382 args = [
383 "build-oci",
384 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
385 "--branch", "lp:foo", "test-image",
386 ]
387 build_oci = parse_args(args=args).operation
388 build_oci.backend.run = FailRepo()
389 self.assertEqual(RETCODE_FAILURE_BUILD, build_oci.run())
390
391 def test_run_build_fails(self):
392 class FailBuild(FakeMethod):
393 def __call__(self, run_args, *args, **kwargs):
394 super(FailBuild, self).__call__(run_args, *args, **kwargs)
395 if run_args[0] == "docker":
396 raise subprocess.CalledProcessError(1, run_args)
397
398 self.useFixture(FakeLogger())
399 args = [
400 "build-oci",
401 "--backend=fake", "--series=xenial", "--arch=amd64", "1",
402 "--branch", "lp:foo", "test-image",
403 ]
404 build_oci = parse_args(args=args).operation
405 build_oci.backend.build_path = self.useFixture(TempDir()).path
406 build_oci.backend.run = FailBuild()
407 self.assertEqual(RETCODE_FAILURE_BUILD, build_oci.run())
0408
=== modified file 'lpbuildd/target/tests/test_build_snap.py'
--- lpbuildd/target/tests/test_build_snap.py 2019-06-05 14:10:20 +0000
+++ lpbuildd/target/tests/test_build_snap.py 2020-02-26 10:52:47 +0000
@@ -186,7 +186,7 @@
186 os.fchmod(proxy_script.fileno(), 0o755)186 os.fchmod(proxy_script.fileno(), 0o755)
187 build_snap.install()187 build_snap.install()
188 self.assertThat(build_snap.backend.run.calls, MatchesListwise([188 self.assertThat(build_snap.backend.run.calls, MatchesListwise([
189 RanAptGet("install", "git", "python3", "socat", "snapcraft"),189 RanAptGet("install", "python3", "socat", "git", "snapcraft"),
190 RanCommand(["mkdir", "-p", "/root/.subversion"]),190 RanCommand(["mkdir", "-p", "/root/.subversion"]),
191 ]))191 ]))
192 self.assertEqual(192 self.assertEqual(
193193
=== modified file 'lpbuildd/target/tests/test_lxd.py'
--- lpbuildd/target/tests/test_lxd.py 2019-12-09 11:17:14 +0000
+++ lpbuildd/target/tests/test_lxd.py 2020-02-26 10:52:47 +0000
@@ -276,7 +276,7 @@
276 "lp-xenial-amd64", "lp-xenial-amd64")276 "lp-xenial-amd64", "lp-xenial-amd64")
277277
278 def assert_correct_profile(self, extra_raw_lxc_config=None,278 def assert_correct_profile(self, extra_raw_lxc_config=None,
279 driver_version="2.0"):279 driver_version="2.0"):
280 if extra_raw_lxc_config is None:280 if extra_raw_lxc_config is None:
281 extra_raw_lxc_config = []281 extra_raw_lxc_config = []
282282
@@ -356,7 +356,7 @@
356 }356 }
357 LXD("1", "xenial", "powerpc").create_profile()357 LXD("1", "xenial", "powerpc").create_profile()
358 self.assert_correct_profile(358 self.assert_correct_profile(
359 extra_raw_lxc_config=[("lxc.seccomp", ""),],359 extra_raw_lxc_config=[("lxc.seccomp", ""), ],
360 driver_version=driver_version or "3.0"360 driver_version=driver_version or "3.0"
361 )361 )
362362
@@ -463,7 +463,8 @@
463 "b", "7", str(minor)]))463 "b", "7", str(minor)]))
464 if not with_dm0:464 if not with_dm0:
465 expected_args.extend([465 expected_args.extend([
466 Equals(["sudo", "dmsetup", "create", "tmpdevice", "--notable"]),466 Equals(
467 ["sudo", "dmsetup", "create", "tmpdevice", "--notable"]),
467 Equals(["sudo", "dmsetup", "remove", "tmpdevice"]),468 Equals(["sudo", "dmsetup", "remove", "tmpdevice"]),
468 ])469 ])
469 for minor in range(8):470 for minor in range(8):
470471
=== added file 'lpbuildd/tests/oci_tarball.py'
--- lpbuildd/tests/oci_tarball.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/tests/oci_tarball.py 2020-02-26 10:52:47 +0000
@@ -0,0 +1,60 @@
1import json
2import os
3import StringIO
4import tempfile
5import tarfile
6
7
8class OCITarball:
9 """Create a tarball for use in tests with OCI."""
10
11 def _makeFile(self, contents, name):
12 json_contents = json.dumps(contents)
13 tarinfo = tarfile.TarInfo(name)
14 tarinfo.size = len(json_contents)
15 return tarinfo, StringIO.StringIO(json_contents)
16
17 @property
18 def config(self):
19 return self._makeFile(
20 {"rootfs": {"diff_ids": ["sha256:diff1", "sha256:diff2"]}},
21 'config.json')
22
23 @property
24 def manifest(self):
25 return self._makeFile(
26 [{"Config": "config.json",
27 "Layers": ["layer-1/layer.tar", "layer-2/layer.tar"]}],
28 'manifest.json')
29
30 @property
31 def repositories(self):
32 return self._makeFile([], 'repositories')
33
34 def layer_file(self, directory, layer_name):
35 contents = "{}-contents".format(layer_name)
36 tarinfo = tarfile.TarInfo(contents)
37 tarinfo.size = len(contents)
38 layer_contents = StringIO.StringIO(contents)
39 layer_tar_path = os.path.join(
40 directory, '{}.tar.gz'.format(layer_name))
41 layer_tar = tarfile.open(layer_tar_path, 'w:gz')
42 layer_tar.addfile(tarinfo, layer_contents)
43 layer_tar.close()
44 return layer_tar_path
45
46 def build_tar_file(self):
47 tar_directory = tempfile.mkdtemp()
48 tar_path = os.path.join(tar_directory, 'test-oci-image.tar')
49 tar = tarfile.open(tar_path, 'w')
50 tar.addfile(*self.config)
51 tar.addfile(*self.manifest)
52 tar.addfile(*self.repositories)
53
54 for layer_name in ['layer-1', 'layer-2']:
55 layer = self.layer_file(tar_directory, layer_name)
56 tar.add(layer, arcname='{}.tar.gz'.format(layer_name))
57
58 tar.close()
59
60 return tar_path
061
=== added file 'lpbuildd/tests/test_oci.py'
--- lpbuildd/tests/test_oci.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/tests/test_oci.py 2020-02-26 10:52:47 +0000
@@ -0,0 +1,280 @@
1# Copyright 2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6import io
7import json
8import os
9try:
10 from unittest import mock
11except ImportError:
12 import mock
13
14from fixtures import (
15 EnvironmentVariable,
16 MockPatch,
17 TempDir,
18 )
19from testtools import TestCase
20from testtools.matchers import Contains
21from testtools.deferredruntest import AsynchronousDeferredRunTest
22from twisted.internet import defer
23
24from lpbuildd.oci import (
25 OCIBuildManager,
26 OCIBuildState,
27 )
28from lpbuildd.tests.fakebuilder import FakeBuilder
29from lpbuildd.tests.oci_tarball import OCITarball
30
31
32class MockBuildManager(OCIBuildManager):
33 def __init__(self, *args, **kwargs):
34 super(MockBuildManager, self).__init__(*args, **kwargs)
35 self.commands = []
36 self.iterators = []
37
38 def runSubProcess(self, path, command, iterate=None, env=None):
39 self.commands.append([path] + command)
40 if iterate is None:
41 iterate = self.iterate
42 self.iterators.append(iterate)
43 return 0
44
45
46class MockOCITarSave():
47 @property
48 def stdout(self):
49 return io.open(OCITarball().build_tar_file(), 'rb')
50
51
52class TestOCIBuildManagerIteration(TestCase):
53 """Run OCIBuildManager through its iteration steps."""
54
55 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
56
57 def setUp(self):
58 super(TestOCIBuildManagerIteration, self).setUp()
59 self.working_dir = self.useFixture(TempDir()).path
60 builder_dir = os.path.join(self.working_dir, "builder")
61 home_dir = os.path.join(self.working_dir, "home")
62 for dir in (builder_dir, home_dir):
63 os.mkdir(dir)
64 self.useFixture(EnvironmentVariable("HOME", home_dir))
65 self.builder = FakeBuilder(builder_dir)
66 self.buildid = "123"
67 self.buildmanager = MockBuildManager(self.builder, self.buildid)
68 self.buildmanager._cachepath = self.builder._cachepath
69
70 def getState(self):
71 """Retrieve build manager's state."""
72 return self.buildmanager._state
73
74 @defer.inlineCallbacks
75 def startBuild(self, args=None, options=None):
76 # The build manager's iterate() kicks off the consecutive states
77 # after INIT.
78 extra_args = {
79 "series": "xenial",
80 "arch_tag": "i386",
81 "name": "test-image",
82 }
83 if args is not None:
84 extra_args.update(args)
85 original_backend_name = self.buildmanager.backend_name
86 self.buildmanager.backend_name = "fake"
87 self.buildmanager.initiate({}, "chroot.tar.gz", extra_args)
88 self.buildmanager.backend_name = original_backend_name
89
90 # Skip states that are done in DebianBuildManager to the state
91 # directly before BUILD_OCI.
92 self.buildmanager._state = OCIBuildState.UPDATE
93
94 # BUILD_OCI: Run the builder's payload to build the snap package.
95 yield self.buildmanager.iterate(0)
96 self.assertEqual(OCIBuildState.BUILD_OCI, self.getState())
97 expected_command = [
98 "sharepath/bin/in-target", "in-target", "build-oci",
99 "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid,
100 ]
101 if options is not None:
102 expected_command.extend(options)
103 expected_command.append("test-image")
104 self.assertEqual(expected_command, self.buildmanager.commands[-1])
105 self.assertEqual(
106 self.buildmanager.iterate, self.buildmanager.iterators[-1])
107 self.assertFalse(self.builder.wasCalled("chrootFail"))
108
109 @defer.inlineCallbacks
110 def test_iterate(self):
111 # This sha would change as it includes file attributes in the
112 # tar file. Fix it so we can test against a known value.
113 sha_mock = self.useFixture(
114 MockPatch('lpbuildd.oci.OCIBuildManager._calculateLayerSha'))
115 sha_mock.mock.return_value = "testsha"
116 # The build manager iterates a normal build from start to finish.
117 args = {
118 "git_repository": "https://git.launchpad.dev/~example/+git/snap",
119 "git_path": "master",
120 }
121 expected_options = [
122 "--git-repository", "https://git.launchpad.dev/~example/+git/snap",
123 "--git-path", "master",
124 ]
125 yield self.startBuild(args, expected_options)
126
127 log_path = os.path.join(self.buildmanager._cachepath, "buildlog")
128 with open(log_path, "w") as log:
129 log.write("I am a build log.")
130
131 self.buildmanager.backend.run.result = MockOCITarSave()
132
133 self.buildmanager.backend.add_file(
134 '/var/lib/docker/image/'
135 'vfs/distribution/v2metadata-by-diffid/sha256/diff1',
136 b"""[{"Digest": "test_digest", "SourceRepository": "test"}]"""
137 )
138
139 # After building the package, reap processes.
140 yield self.buildmanager.iterate(0)
141 expected_command = [
142 "sharepath/bin/in-target", "in-target", "scan-for-processes",
143 "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid,
144 ]
145 self.assertEqual(OCIBuildState.BUILD_OCI, self.getState())
146 self.assertEqual(expected_command, self.buildmanager.commands[-1])
147 self.assertNotEqual(
148 self.buildmanager.iterate, self.buildmanager.iterators[-1])
149 self.assertFalse(self.builder.wasCalled("buildFail"))
150 expected_files = [
151 'manifest.json',
152 'layer-1.tar.gz',
153 'layer-2.tar.gz',
154 'digests.json',
155 'config.json',
156 ]
157 for expected in expected_files:
158 self.assertThat(self.builder.waitingfiles, Contains(expected))
159
160 cache_path = self.builder.cachePath(
161 self.builder.waitingfiles['digests.json'])
162 with open(cache_path, "rb") as f:
163 digests_contents = f.read()
164 digests_expected = [{
165 "sha256:diff1": {
166 "source": "test",
167 "digest": "test_digest",
168 "layer_id": "layer-1"
169 },
170 "sha256:diff2": {
171 "source": "",
172 "digest": "testsha",
173 "layer_id": "layer-2"
174 }
175 }]
176 self.assertEqual(digests_contents, json.dumps(digests_expected))
177 # Control returns to the DebianBuildManager in the UMOUNT state.
178 self.buildmanager.iterateReap(self.getState(), 0)
179 expected_command = [
180 "sharepath/bin/in-target", "in-target", "umount-chroot",
181 "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid,
182 ]
183 self.assertEqual(OCIBuildState.UMOUNT, self.getState())
184 self.assertEqual(expected_command, self.buildmanager.commands[-1])
185 self.assertEqual(
186 self.buildmanager.iterate, self.buildmanager.iterators[-1])
187 self.assertFalse(self.builder.wasCalled("buildFail"))
188
189 @defer.inlineCallbacks
190 def test_iterate_with_file(self):
191 # This sha would change as it includes file attributes in the
192 # tar file. Fix it so we can test against a known value.
193 sha_mock = self.useFixture(
194 MockPatch('lpbuildd.oci.OCIBuildManager._calculateLayerSha'))
195 sha_mock.mock.return_value = "testsha"
196 # The build manager iterates a build that specifies a non-default
197 # Dockerfile location from start to finish.
198 args = {
199 "git_repository": "https://git.launchpad.dev/~example/+git/snap",
200 "git_path": "master",
201 "build_file": "build-aux/Dockerfile",
202 }
203 expected_options = [
204 "--git-repository", "https://git.launchpad.dev/~example/+git/snap",
205 "--git-path", "master",
206 "--build-file", "build-aux/Dockerfile",
207 ]
208 yield self.startBuild(args, expected_options)
209
210 log_path = os.path.join(self.buildmanager._cachepath, "buildlog")
211 with open(log_path, "w") as log:
212 log.write("I am a build log.")
213
214 self.buildmanager.backend.run.result = MockOCITarSave()
215
216 self.buildmanager.backend.add_file(
217 '/var/lib/docker/image/'
218 'vfs/distribution/v2metadata-by-diffid/sha256/diff1',
219 b"""[{"Digest": "test_digest", "SourceRepository": "test"}]"""
220 )
221
222 # After building the package, reap processes.
223 yield self.buildmanager.iterate(0)
224 expected_command = [
225 "sharepath/bin/in-target", "in-target", "scan-for-processes",
226 "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid,
227 ]
228 self.assertEqual(OCIBuildState.BUILD_OCI, self.getState())
229 self.assertEqual(expected_command, self.buildmanager.commands[-1])
230 self.assertNotEqual(
231 self.buildmanager.iterate, self.buildmanager.iterators[-1])
232 self.assertFalse(self.builder.wasCalled("buildFail"))
233 expected_files = [
234 'manifest.json',
235 'layer-1.tar.gz',
236 'layer-2.tar.gz',
237 'digests.json',
238 'config.json',
239 ]
240 for expected in expected_files:
241 self.assertThat(self.builder.waitingfiles, Contains(expected))
242
243 cache_path = self.builder.cachePath(
244 self.builder.waitingfiles['digests.json'])
245 with open(cache_path, "rb") as f:
246 digests_contents = f.read()
247 digests_expected = [{
248 "sha256:diff1": {
249 "source": "test",
250 "digest": "test_digest",
251 "layer_id": "layer-1"
252 },
253 "sha256:diff2": {
254 "source": "",
255 "digest": "testsha",
256 "layer_id": "layer-2"
257 }
258 }]
259 self.assertEqual(digests_contents, json.dumps(digests_expected))
260
261 # Control returns to the DebianBuildManager in the UMOUNT state.
262 self.buildmanager.iterateReap(self.getState(), 0)
263 expected_command = [
264 "sharepath/bin/in-target", "in-target", "umount-chroot",
265 "--backend=lxd", "--series=xenial", "--arch=i386", self.buildid,
266 ]
267 self.assertEqual(OCIBuildState.UMOUNT, self.getState())
268 self.assertEqual(expected_command, self.buildmanager.commands[-1])
269 self.assertEqual(
270 self.buildmanager.iterate, self.buildmanager.iterators[-1])
271 self.assertFalse(self.builder.wasCalled("buildFail"))
272
273 @defer.inlineCallbacks
274 def test_iterate_snap_store_proxy(self):
275 # The build manager can be told to use a snap store proxy.
276 self.builder._config.set(
277 "proxy", "snapstore", "http://snap-store-proxy.example/")
278 expected_options = [
279 "--snap-store-proxy-url", "http://snap-store-proxy.example/"]
280 yield self.startBuild(options=expected_options)

Subscribers

People subscribed via source and target branches