Merge ~cjwatson/launchpad:build-tarball into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 16fd4653996f512c728b80d72dcd75cd51ec2df2
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:build-tarball
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:version-info-outside-git
Diff against target: 343 lines (+270/-10)
4 files modified
Makefile (+56/-10)
ols-vms.conf (+11/-0)
utilities/build-tarball (+43/-0)
utilities/publish-to-swift (+160/-0)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+412692@code.launchpad.net

Commit message

Support publishing deployment artifact to Swift

Description of the change

This adds `build-tarball` and `publish-tarball` make targets, which will help us to write a Jenkins job that builds a deployment artifact and publishes it to Swift for use by future deployment machinery.

The new `ols-vms.conf` file is to assist integration with lp:ols-jenkaas, which is where we currently do publication to Swift for other projects.

Aside from the new build and publish scripts, I had to adjust the existing wheel-building target a bit to also build wheels for the packages in `requirements/setup.txt`.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

LGTM, given that I have not much knowledge about swift (what about a quick introduction at one of the next standups?)

Also, I wonder about the big picture of this MP.

I thought future deployment will be done with Juju/Charms, so there would be no more need for interwoven bash scripts and make files?

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

Swift is OpenStack's distributed object store component. See https://docs.openstack.org/swift/latest/ for documentation. In PS5 the same API is provided by ceph-radosgw (https://docs.ceph.com/en/latest/radosgw/index.html), but it ends up being much the same. We use it for the Launchpad librarian, but it's also a handy way to store blobs for deployment.

Juju charms will indeed eventually let us refactor a whole bunch of confusing deployment machinery, but they don't help with the preliminary step of building code artifacts for deployment; that still has to be done separately one way or another. This work will replace something that's currently done by a combination of carob:~pqm/bin/update-launchpad-built-code.sh (which builds a tree) and lp:deployment-manager (which among other things rsyncs that tree out to target machines), as well as some even more confusing and less coherent things on (qa)staging.

Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Thanks for the detailed replies!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/Makefile b/Makefile
2index 8b00a2e..69ad953 100644
3--- a/Makefile
4+++ b/Makefile
5@@ -15,6 +15,8 @@ PY=$(WD)/bin/py
6 PYTHONPATH:=$(WD)/lib:${PYTHONPATH}
7 VERBOSITY=-vv
8
9+DEPENDENCY_REPO ?= https://git.launchpad.net/lp-source-dependencies
10+
11 # virtualenv and pip fail if setlocale fails, so force a valid locale.
12 PIP_ENV := LC_ALL=C.UTF-8
13 # Run with "make PIP_NO_INDEX=" if you want pip to find software
14@@ -95,6 +97,20 @@ PIP_BIN = \
15 bin/watch_jsbuild \
16 bin/with-xvfb
17
18+# Create archives in labelled directories (e.g.
19+# <rev-id>/$(PROJECT_NAME).tar.gz)
20+TARBALL_BUILD_LABEL ?= $(shell git rev-parse HEAD)
21+TARBALL_FILE_NAME = launchpad.tar.gz
22+TARBALL_BUILD_DIR = dist/$(TARBALL_BUILD_LABEL)
23+TARBALL_BUILD_PATH = $(TARBALL_BUILD_DIR)/$(TARBALL_FILE_NAME)
24+
25+SWIFT_CONTAINER_NAME ?= launchpad-builds
26+# This must match the object path used by fetch_payload in the ols charm
27+# layer.
28+SWIFT_OBJECT_PATH = \
29+ launchpad-builds/$(TARBALL_BUILD_LABEL)/$(TARBALL_FILE_NAME)
30+
31+
32 # DO NOT ALTER : this should just build by default
33 .PHONY: default
34 default: inplace
35@@ -173,6 +189,17 @@ inplace: build logs clean_logs codehosting-dir
36 .PHONY: build
37 build: compile apidoc jsbuild css_combine
38
39+# Bootstrap download-cache and sourcecode. Useful for CI jobs that want to
40+# set these up from scratch.
41+.PHONY: bootstrap
42+bootstrap:
43+ if [ -d download-cache/.git ]; then \
44+ git -C download-cache pull; \
45+ else \
46+ git clone --depth=1 $(DEPENDENCY_REPO) download-cache; \
47+ fi
48+ utilities/update-sourcecode
49+
50 # LP_SOURCEDEPS_PATH should point to the sourcecode directory, but we
51 # want the parent directory where the download-cache and env directories
52 # are. We re-use the variable that is using for the rocketfuel-get script.
53@@ -258,29 +285,48 @@ requirements/combined.txt: \
54 --include requirements/launchpad.txt \
55 >"$@"
56
57-# This target is used by LOSAs to prepare a build to be pushed out to
58-# destination machines. We only want wheels: they are the expensive bits,
59-# and the other bits might run into problems like bug 575037. This target
60-# runs pip, builds a wheelhouse with predictable paths that can be used even
61-# if the build is pushed to a different path on the destination machines,
62-# and then removes everything created except for the wheels.
63-#
64 # It doesn't seem to be straightforward to build a wheelhouse of all our
65 # dependencies without also building a useless wheel of Launchpad itself;
66 # fortunately that doesn't take too long, and we just remove it afterwards.
67-.PHONY: build_wheels
68-build_wheels: $(PIP_BIN) requirements/combined.txt
69+.PHONY: build_wheels_only
70+build_wheels_only: $(PIP_BIN) requirements/combined.txt
71 $(RM) -r wheelhouse wheels
72+ $(SHHH) $(PIP) wheel -w wheels -r requirements/setup.txt
73 $(SHHH) $(PIP) wheel \
74 -c requirements/setup.txt -c requirements/combined.txt \
75 -w wheels .
76 $(RM) wheels/lp-[0-9]*.whl
77- $(MAKE) clean_pip
78+
79+# This target is used by deployment machinery to prepare a build to be
80+# pushed out to destination machines. We only want wheels: they are the
81+# expensive bits, and the other bits might run into problems like bug
82+# 575037. This target runs pip, builds a wheelhouse with predictable paths
83+# that can be used even if the build is pushed to a different path on the
84+# destination machines, and then removes everything created except for the
85+# wheels.
86+.PHONY: build_wheels
87+build_wheels: build_wheels_only
88+ $(MAKE) clean_js clean_pip
89
90 # Compatibility
91 .PHONY: build_eggs
92 build_eggs: build_wheels
93
94+# Build a tarball that can be unpacked and built on another machine,
95+# including all the wheels we need. This will eventually supersede
96+# build_wheels.
97+.PHONY: build-tarball
98+build-tarball:
99+ utilities/build-tarball $(TARBALL_BUILD_DIR)
100+
101+# Publish a buildable tarball to Swift.
102+.PHONY: publish-tarball
103+publish-tarball: build-tarball
104+ [ ! -e ~/.config/swift/launchpad ] || . ~/.config/swift/launchpad; \
105+ utilities/publish-to-swift --debug \
106+ $(SWIFT_CONTAINER_NAME) $(SWIFT_OBJECT_PATH) \
107+ $(TARBALL_BUILD_PATH)
108+
109 # setuptools won't touch files that would have the same contents, but for
110 # Make's sake we need them to get fresh timestamps, so we touch them after
111 # building.
112diff --git a/ols-vms.conf b/ols-vms.conf
113new file mode 100644
114index 0000000..cd26bce
115--- /dev/null
116+++ b/ols-vms.conf
117@@ -0,0 +1,11 @@
118+# Options defined here provide defaults for all sections
119+vm.architecture = amd64
120+vm.release = xenial
121+
122+apt.sources = ppa:launchpad/ppa
123+vm.packages = launchpad-dependencies
124+
125+[launchpad]
126+vm.class = lxd
127+vm.update = True
128+jenkaas.secrets = swift/launchpad:.config/swift/launchpad
129diff --git a/utilities/build-tarball b/utilities/build-tarball
130new file mode 100755
131index 0000000..6923320
132--- /dev/null
133+++ b/utilities/build-tarball
134@@ -0,0 +1,43 @@
135+#! /bin/sh
136+#
137+# Copyright 2021 Canonical Ltd. This software is licensed under the
138+# GNU Affero General Public License version 3 (see the file LICENSE).
139+
140+set -e
141+
142+if [ -z "$1" ]; then
143+ echo "Usage: $0 OUTPUT-DIRECTORY" >&2
144+ exit 2
145+fi
146+output_dir="$1"
147+
148+# Build wheels to include in the distributed tarball.
149+# XXX cjwatson 2021-12-10: It's unfortunate that this script is called from
150+# the Makefile and also has to call out to the Makefile, but this is
151+# difficult to disentangle until we refactor our build system to use
152+# something higher-level than pip.
153+make build_wheels_only
154+
155+# Ensure that we have an updated idea of this tree's version.
156+scripts/update-version-info.sh
157+
158+echo "Creating deployment tarball in $output_dir"
159+
160+# Prepare a file list.
161+mkdir -p "$output_dir"
162+(
163+ git ls-files | sed 's,^,./,'
164+ # Most of download-cache is unnecessary since we include a wheelhouse
165+ # instead, but JavaScript-enabled builds need yarn.
166+ find ./download-cache/dist/ -name yarn-\*.tar.gz
167+ # The subdirectories of sourcecode may be symlinks. Force tar to
168+ # include the actual files instead.
169+ find ./sourcecode/*/ \
170+ \( -name .bzr -o -name __pycache__ \) -prune -o -type f -print
171+ echo ./version-info.py
172+ find ./wheels/ -name \*.whl -print
173+) | sort >"$output_dir/.files"
174+
175+# Create the tarball.
176+tar -czf "$output_dir/launchpad.tar.gz" --no-recursion \
177+ --files-from "$output_dir/.files"
178diff --git a/utilities/publish-to-swift b/utilities/publish-to-swift
179new file mode 100755
180index 0000000..84de4c1
181--- /dev/null
182+++ b/utilities/publish-to-swift
183@@ -0,0 +1,160 @@
184+#! /usr/bin/python3 -S
185+#
186+# Copyright 2021 Canonical Ltd. This software is licensed under the
187+# GNU Affero General Public License version 3 (see the file LICENSE).
188+
189+"""Publish a built tarball to Swift for deployment."""
190+
191+import _pythonpath # noqa: F401
192+
193+from argparse import ArgumentParser
194+import os
195+
196+import iso8601
197+import requests
198+from swiftclient.service import (
199+ get_conn,
200+ process_options,
201+ SwiftService,
202+ SwiftUploadObject,
203+ )
204+from swiftclient.shell import add_default_args
205+
206+
207+def ensure_container_privs(options, container_name):
208+ """Ensure that the container exists and is world-readable.
209+
210+ This allows us to give services suitable credentials for getting the
211+ built code from a container.
212+ """
213+ options = dict(options)
214+ options["read_acl"] = ".r:*"
215+ with SwiftService(options=options) as swift:
216+ swift.post(container=container_name)
217+
218+
219+def get_swift_storage_url(options):
220+ """Return the storage URL under which Swift objects are published."""
221+ return get_conn(options).get_auth()[0]
222+
223+
224+def publish_file_to_swift(options, container_name, object_path, local_path,
225+ overwrite=True):
226+ """Publish a file to a Swift container."""
227+ storage_url = get_swift_storage_url(options)
228+
229+ with SwiftService(options=options) as swift:
230+ stat_results = swift.stat(
231+ container=container_name, objects=[object_path])
232+ if stat_results and next(stat_results)["success"]:
233+ print("Object {} already published to {}.".format(
234+ object_path, container_name))
235+ if not overwrite:
236+ return
237+
238+ print("Publishing {} to {} as {}.".format(
239+ local_path, container_name, object_path))
240+ for r in swift.upload(
241+ container_name,
242+ [SwiftUploadObject(local_path, object_name=object_path)]):
243+ if not r["success"]:
244+ raise r["error"]
245+
246+ print("Published file: {}/{}/{}".format(
247+ storage_url, container_name, object_path))
248+
249+
250+def prune_old_files_from_swift(options, container_name, object_dir):
251+ """Prune files from Swift that we no longer need."""
252+ response = requests.head("https://launchpad.net/")
253+ response.raise_for_status()
254+ production_revision = response.headers["X-VCS-Revision"]
255+
256+ with SwiftService(options=options) as swift:
257+ objs = {}
258+ production_mtime = None
259+ for stats in swift.list(
260+ container=container_name,
261+ options={"prefix": "{}/".format(object_dir)}):
262+ if not stats["success"]:
263+ raise stats["error"]
264+ for item in stats["listing"]:
265+ if item.get("subdir") is None:
266+ mtime = iso8601.parse_date(item["last_modified"])
267+ objs[item["name"]] = mtime
268+ if item["name"].startswith(
269+ "{}/{}/".format(object_dir, production_revision)):
270+ production_mtime = mtime
271+
272+ if production_mtime is None:
273+ print(
274+ "No file in {} corresponding to production revision {}; "
275+ "not pruning.".format(container_name, production_revision))
276+ return
277+
278+ for object_name, mtime in sorted(objs.items()):
279+ if mtime < production_mtime:
280+ print("Pruning {} (older than production)".format(object_name))
281+ for r in swift.delete(
282+ container=container_name, objects=[object_name]):
283+ if not r["success"]:
284+ raise r["error"]
285+
286+
287+def main():
288+ parser = ArgumentParser()
289+ parser.add_argument("container_name")
290+ parser.add_argument("swift_object_path")
291+ parser.add_argument("local_path")
292+ add_default_args(parser)
293+ args = parser.parse_args()
294+
295+ if args.debug:
296+ # Print OpenStack-related environment variables for ease of
297+ # debugging. Only OS_AUTH_TOKEN and OS_PASSWORD currently seem to
298+ # be secret, but for safety we only show unredacted contents of
299+ # variables specifically known to be safe. See "swift --os-help"
300+ # for most of these.
301+ safe_keys = {
302+ "OS_AUTH_URL",
303+ "OS_AUTH_VERSION",
304+ "OS_CACERT",
305+ "OS_CERT",
306+ "OS_ENDPOINT_TYPE",
307+ "OS_IDENTITY_API_VERSION",
308+ "OS_INTERFACE",
309+ "OS_KEY",
310+ "OS_PROJECT_DOMAIN_ID",
311+ "OS_PROJECT_DOMAIN_NAME",
312+ "OS_PROJECT_ID",
313+ "OS_PROJECT_NAME",
314+ "OS_REGION_NAME",
315+ "OS_SERVICE_TYPE",
316+ "OS_STORAGE_URL",
317+ "OS_TENANT_ID",
318+ "OS_TENANT_NAME",
319+ "OS_USERNAME",
320+ "OS_USER_DOMAIN_ID",
321+ "OS_USER_DOMAIN_NAME",
322+ "OS_USER_ID",
323+ }
324+ for key, value in sorted(os.environ.items()):
325+ if key.startswith("OS_"):
326+ if key not in safe_keys:
327+ value = "<redacted>"
328+ print("{}: {}".format(key, value))
329+
330+ options = vars(args)
331+ process_options(options)
332+
333+ overwrite = "FORCE_REBUILD" in os.environ
334+ ensure_container_privs(options, args.container_name)
335+ publish_file_to_swift(
336+ options, args.container_name, args.swift_object_path, args.local_path,
337+ overwrite=overwrite)
338+ prune_old_files_from_swift(
339+ options, args.container_name, args.swift_object_path.split("/")[0])
340+
341+
342+if __name__ == "__main__":
343+ main()

Subscribers

People subscribed via source and target branches

to status/vote changes: