Merge ~cjwatson/launchpad:artifactory-multiple-orig into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 739632f684995ae1784d7d1ec4530e1c76cb833e
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:artifactory-multiple-orig
Merge into: launchpad:master
Diff against target: 325 lines (+139/-23)
4 files modified
lib/lp/archivepublisher/artifactory.py (+4/-5)
lib/lp/archivepublisher/publishing.py (+48/-5)
lib/lp/archivepublisher/tests/test_artifactory.py (+13/-13)
lib/lp/archivepublisher/tests/test_publisher.py (+74/-0)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+435618@code.launchpad.net

Commit message

Fix handling of shared files when updating Artifactory properties

Description of the change

Some files (notably `.orig.tar.*` files in Debian-format source packages) may be shared between multiple different package versions: for example, `hello 1.0-1` and `hello 1.0-2` will normally share a file called something like `hello_1.0.orig.tar.gz` containing the original upstream source code.

For Artifactory publication, the publisher handled this case poorly. If two versions were published that shared the same file, but then one of those versions was removed, the shared file was correctly left in place until there are no remaining publications using it; but `Publisher.C_updateArtifactoryProperties` attempted to update properties on all the files associated with the removed version rather than on just the file that was still published, resulting in a `FileNotFoundError`.

Fixing this requires working out the `IPackageReleaseFile` and `IPublishingView` objects to which a given Artifactory path "belongs", which is somewhat complex: we have to go through all the possible release objects, figure out what the paths of all their files would be if published, build mappings of paths back to files and publications, and use those mappings to update only the paths that are still published with the correct properties.

The shared files end up still having `launchpad.release-id` properties associated with the old version, but this seems to be a minor enough problem that we can ignore it for now.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/archivepublisher/artifactory.py b/lib/lp/archivepublisher/artifactory.py
2index 2d16d99..b0fd664 100644
3--- a/lib/lp/archivepublisher/artifactory.py
4+++ b/lib/lp/archivepublisher/artifactory.py
5@@ -12,7 +12,7 @@ import os
6 import tempfile
7 from collections import defaultdict
8 from pathlib import Path, PurePath
9-from typing import TYPE_CHECKING, List, Optional
10+from typing import TYPE_CHECKING, Optional
11 from urllib.parse import quote_plus
12
13 import requests
14@@ -553,14 +553,13 @@ class ArtifactoryPool:
15 self,
16 source_name: str,
17 source_version: str,
18- pub_files: List[IPackageReleaseFile],
19+ pub_file: IPackageReleaseFile,
20 publications,
21 old_properties=None,
22 ):
23 """Update a file's properties in Artifactory."""
24- for pub_file in pub_files:
25- entry = self._getEntry(source_name, source_version, pub_file)
26- entry.updateProperties(publications, old_properties=old_properties)
27+ entry = self._getEntry(source_name, source_version, pub_file)
28+ entry.updateProperties(publications, old_properties=old_properties)
29
30 def getArtifactPatterns(self, repository_format):
31 """Get patterns matching artifacts in a repository of this format.
32diff --git a/lib/lp/archivepublisher/publishing.py b/lib/lp/archivepublisher/publishing.py
33index b1faa80..5778aa9 100644
34--- a/lib/lp/archivepublisher/publishing.py
35+++ b/lib/lp/archivepublisher/publishing.py
36@@ -24,6 +24,7 @@ from functools import partial
37 from itertools import chain, groupby
38 from operator import attrgetter
39
40+from artifactory import ArtifactoryPath
41 from debian.deb822 import Release, _multivalued
42 from storm.expr import Desc
43 from zope.component import getUtility
44@@ -817,7 +818,13 @@ class Publisher:
45 # Skip any files that Launchpad didn't put in Artifactory.
46 continue
47 plan.append(
48- (source_name[0], source_version[0], release_id[0], properties)
49+ (
50+ source_name[0],
51+ source_version[0],
52+ release_id[0],
53+ path,
54+ properties,
55+ )
56 )
57
58 # Releases that have been removed may still have corresponding
59@@ -826,7 +833,7 @@ class Publisher:
60 # corresponding pool entries.
61 missing_sources = set()
62 missing_binaries = set()
63- for _, _, release_id, _ in plan:
64+ for _, _, release_id, _, _ in plan:
65 if release_id in releases_by_id:
66 continue
67 match = re.match(r"^source:(\d+)$", release_id)
68@@ -841,12 +848,48 @@ class Publisher:
69 for bpr in load(BinaryPackageRelease, missing_binaries):
70 releases_by_id["binary:%d" % bpr.id] = bpr
71
72- for source_name, source_version, release_id, properties in plan:
73+ # Work out the publication files and publications that each path
74+ # belongs to (usually just one, but there are cases where one file
75+ # may be shared among multiple releases, such as .orig.tar.* files
76+ # in Debian-format source packages).
77+ pub_files_by_path = defaultdict(set)
78+ pubs_by_path = defaultdict(set)
79+ for source_name, source_version, release_id, _, _ in plan:
80+ for pub_file in releases_by_id[release_id].files:
81+ path = self._diskpool.pathFor(
82+ None, source_name, source_version, pub_file
83+ )
84+ pub_files_by_path[path].add(pub_file)
85+ if release_id in pubs_by_id:
86+ pubs_by_path[path].update(pubs_by_id[release_id])
87+
88+ root_path = ArtifactoryPath(self._config.archiveroot)
89+ for source_name, source_version, _, path, properties in plan:
90+ full_path = root_path / path
91+ # For now, any of the possible publication files matching this
92+ # path should do just as well; it's only used to work out the
93+ # artifact path. Just to make sure things are deterministic,
94+ # use the one with the lowest ID.
95+ pub_file = sorted(
96+ pub_files_by_path[full_path], key=attrgetter("id")
97+ )[0]
98+ # Tell the pool about all the publications that refer to this
99+ # file, since it may set some properties to describe the union
100+ # of all of them.
101+ publications = sorted(
102+ pubs_by_path.get(full_path, []), key=attrgetter("id")
103+ )
104+ # Use the name and version from the first publication if we can,
105+ # but if there aren't any then fall back to the property values
106+ # from Artifactory.
107+ if publications:
108+ source_name = publications[0].pool_name
109+ source_version = publications[0].pool_version
110 self._diskpool.updateProperties(
111 source_name,
112 source_version,
113- releases_by_id[release_id].files,
114- pubs_by_id.get(release_id),
115+ pub_file,
116+ publications,
117 old_properties=properties,
118 )
119
120diff --git a/lib/lp/archivepublisher/tests/test_artifactory.py b/lib/lp/archivepublisher/tests/test_artifactory.py
121index da1a31a..924d12c 100644
122--- a/lib/lp/archivepublisher/tests/test_artifactory.py
123+++ b/lib/lp/archivepublisher/tests/test_artifactory.py
124@@ -536,7 +536,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
125 },
126 path.properties,
127 )
128- pool.updateProperties(spr.name, spr.version, [sprf], spphs)
129+ pool.updateProperties(spr.name, spr.version, sprf, spphs)
130 self.assertEqual(
131 {
132 "launchpad.release-id": ["source:%d" % spr.id],
133@@ -612,7 +612,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
134 path.properties,
135 )
136 pool.updateProperties(
137- bpr.sourcepackagename, bpr.sourcepackageversion, [bpf], bpphs
138+ bpr.sourcepackagename, bpr.sourcepackageversion, bpf, bpphs
139 )
140 self.assertEqual(
141 {
142@@ -682,7 +682,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
143 path.properties,
144 )
145 pool.updateProperties(
146- bpr.sourcepackagename, bpr.sourcepackageversion, [bpf], bpphs
147+ bpr.sourcepackagename, bpr.sourcepackageversion, bpf, bpphs
148 )
149 self.assertEqual(
150 {
151@@ -759,7 +759,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
152 },
153 path.properties,
154 )
155- pool.updateProperties(spr.name, spr.version, [sprf], spphs)
156+ pool.updateProperties(spr.name, spr.version, sprf, spphs)
157 self.assertEqual(
158 {
159 "launchpad.release-id": ["source:%d" % spr.id],
160@@ -837,7 +837,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
161 },
162 path.properties,
163 )
164- pool.updateProperties(spr.name, spr.version, [sprf], spphs)
165+ pool.updateProperties(spr.name, spr.version, sprf, spphs)
166 self.assertEqual(
167 {
168 "launchpad.release-id": ["source:%d" % spr.id],
169@@ -914,7 +914,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
170 },
171 path.properties,
172 )
173- pool.updateProperties(spr.name, spr.version, [sprf], spphs)
174+ pool.updateProperties(spr.name, spr.version, sprf, spphs)
175 self.assertEqual(
176 {
177 "launchpad.release-id": ["source:%d" % spr.id],
178@@ -1008,7 +1008,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
179 },
180 path.properties,
181 )
182- pool.updateProperties(bpph.pool_name, bpph.pool_version, [bpf], bpphs)
183+ pool.updateProperties(bpph.pool_name, bpph.pool_version, bpf, bpphs)
184 self.assertEqual(
185 {
186 "launchpad.release-id": ["binary:%d" % bpr.id],
187@@ -1093,7 +1093,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
188 },
189 path.properties,
190 )
191- pool.updateProperties(bpph.pool_name, bpph.pool_version, [bpf], bpphs)
192+ pool.updateProperties(bpph.pool_name, bpph.pool_version, bpf, bpphs)
193 self.assertEqual(
194 {
195 "launchpad.release-id": ["binary:%d" % bpr.id],
196@@ -1178,7 +1178,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
197 },
198 path.properties,
199 )
200- pool.updateProperties(bpph.pool_name, bpph.pool_version, [bpf], bpphs)
201+ pool.updateProperties(bpph.pool_name, bpph.pool_version, bpf, bpphs)
202 self.assertEqual(
203 {
204 "launchpad.release-id": ["binary:%d" % bpr.id],
205@@ -1261,7 +1261,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
206 },
207 path.properties,
208 )
209- pool.updateProperties(spr.name, spr.version, [sprf], spphs)
210+ pool.updateProperties(spr.name, spr.version, sprf, spphs)
211 self.assertEqual(
212 {
213 "generic.name": ["foo"],
214@@ -1350,7 +1350,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
215 },
216 path.properties,
217 )
218- pool.updateProperties(bpph.pool_name, bpph.pool_version, [bpf], bpphs)
219+ pool.updateProperties(bpph.pool_name, bpph.pool_version, bpf, bpphs)
220 self.assertEqual(
221 {
222 "generic.name": ["foo"],
223@@ -1423,7 +1423,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
224 path.properties,
225 )
226 pool.updateProperties(
227- bpr.sourcepackagename, bpr.sourcepackageversion, [bpf], bpphs
228+ bpr.sourcepackagename, bpr.sourcepackageversion, bpf, bpphs
229 )
230 self.assertEqual(
231 {
232@@ -1485,7 +1485,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
233 ]["pypi.summary"] = ["text with special characters: ;=|,\\"]
234
235 pool.updateProperties(
236- bpr.sourcepackagename, bpr.sourcepackageversion, [bpf], bpphs
237+ bpr.sourcepackagename, bpr.sourcepackageversion, bpf, bpphs
238 )
239
240 self.assertEqual(
241diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py
242index 7d9abb4..c675a51 100644
243--- a/lib/lp/archivepublisher/tests/test_publisher.py
244+++ b/lib/lp/archivepublisher/tests/test_publisher.py
245@@ -4374,6 +4374,80 @@ class TestArtifactoryPublishing(TestPublisherBase):
246 binary_path.properties,
247 )
248
249+ def test_update_properties_shared_orig(self):
250+ """An .orig from a previous Debian revision doesn't confuse matters."""
251+ self.setUpArtifactory(ArchiveRepositoryFormat.DEBIAN)
252+ publisher = Publisher(
253+ self.logger, self.config, self.disk_pool, self.archive
254+ )
255+ orig_tar_file = self.addMockFile(
256+ "hello_1.0.orig.tar.xz", b"An orig tarball"
257+ )
258+ source_1 = self.getPubSource(
259+ sourcename="hello",
260+ version="1.0-1",
261+ archive=self.archive,
262+ )
263+ source_1.sourcepackagerelease.addFile(
264+ self.addMockFile("hello_1.0-1.debian.tar.xz", b"A tarball")
265+ )
266+ source_1.sourcepackagerelease.addFile(orig_tar_file)
267+ transaction.commit()
268+
269+ publisher.A_publish(False)
270+ publisher.C_updateArtifactoryProperties(False)
271+
272+ # Publish 1.0-2, remove 1.0-1, and simulate process-death-row.
273+ source_2 = self.getPubSource(
274+ sourcename="hello",
275+ version="1.0-2",
276+ archive=self.archive,
277+ )
278+ source_2.sourcepackagerelease.addFile(
279+ self.addMockFile("hello_1.0-2.debian.tar.xz", b"A tarball")
280+ )
281+ source_2.sourcepackagerelease.addFile(orig_tar_file)
282+ source_1.requestDeletion(self.archive.owner)
283+ for pub_file in source_1.files:
284+ if not pub_file.libraryfile.filename.endswith(".orig.tar.xz"):
285+ self.disk_pool.removeFile("main", "hello", "1.0-1", pub_file)
286+ transaction.commit()
287+
288+ publisher.A_publish(False)
289+ publisher.C_updateArtifactoryProperties(False)
290+
291+ for name in "hello_1.0-1.dsc", "hello_1.0-1.debian.tar.xz":
292+ self.assertFalse(
293+ (self.disk_pool.rootpath / "h" / "hello" / name).exists()
294+ )
295+ for spph, name in (
296+ (source_2, "hello_1.0-2.dsc"),
297+ (source_2, "hello_1.0-2.debian.tar.xz"),
298+ # The shared .orig file ends up still having a
299+ # launchpad.release-id property associated with version 1.0-1,
300+ # because ArtifactoryPoolEntry.updateProperties always keeps the
301+ # release-id from the properties passed to it from Artifactory.
302+ # This isn't ideal, but is a relatively minor problem, so allow
303+ # it for now.
304+ (source_1, "hello_1.0.orig.tar.xz"),
305+ ):
306+ self.assertEqual(
307+ {
308+ "deb.component": ["main"],
309+ "deb.distribution": ["breezy-autotest"],
310+ "deb.name": ["hello"],
311+ "deb.version": ["1.0-2"],
312+ "launchpad.release-id": [
313+ "source:%d" % spph.sourcepackagereleaseID
314+ ],
315+ "launchpad.source-name": ["hello"],
316+ "launchpad.source-version": ["1.0-2"],
317+ "soss.license": ["debian/copyright"],
318+ "soss.type": ["source"],
319+ },
320+ (self.disk_pool.rootpath / "h" / "hello" / name).properties,
321+ )
322+
323 def test_remove_properties(self):
324 """We remove properties if a file is no longer published anywhere."""
325 self.setUpArtifactory(ArchiveRepositoryFormat.DEBIAN)

Subscribers

People subscribed via source and target branches

to status/vote changes: