Merge lp:~roadmr/capomastro/single-build into lp:capomastro

Proposed by Daniel Manrique
Status: Merged
Approved by: Daniel Manrique
Approved revision: 205
Merged at revision: 199
Proposed branch: lp:~roadmr/capomastro/single-build
Merge into: lp:capomastro
Diff against target: 352 lines (+219/-16)
6 files modified
docs/source/overview.rst (+49/-0)
docs/source/setup.rst (+14/-0)
projects/forms.py (+1/-1)
projects/helpers.py (+71/-9)
projects/templates/projects/projectbuild_form.html (+3/-4)
projects/tests/test_helpers.py (+81/-2)
To merge this branch: bzr merge lp:~roadmr/capomastro/single-build
Reviewer Review Type Date Requested Status
Caio Begotti (community) Approve
Review via email: mp+262256@code.launchpad.net

Commit message

Create archiveartifacts for non-rebuilt dependencies when building a project (LP: #1450070).

Fix some documentation and view issues.

Description of the change

Essentially this fixes the "partial dependency rebuilds" bug by ensuring that non-rebuilt dependencies have their artifacts properly linked (by creating a new archiveartifact pointing to the old artifact and build). It includes the code to fix this in projects.helpers as well as tests. Caveat: the tests only verify the scenario where a non-rebuilt dependency already had a known good build. They don't test the scenario where a dependency has never been built (which will result in an incomplete set of artifacts, but that's expected).

I also snuck in a few more changes:

Related to the partial dep debuild changes:
70a556e projects:helpers: Test for non-rebuilt dependency behavior
f92c230 projects:helpers: Document arguments for a confusing method
12c6b7b projects:helpers: Variable renaming in build_project for clarity.
bc5485a projects:helpers: non-rebuilt dependencies should have ArchiveArtifacts created from the dependency's last successful build

Two view fixes sent by Caio:
dabbdcc Added a textual description of what happens during the build process
9f6f044 Fix labels in project build form and template

Small documentation tweaks
b878458 docs: Clarify deb packages that need to be installed before running the next step

To post a comment you must log in.
Revision history for this message
Caio Begotti (caio1982) wrote :

Just finished testing it all on Wendigo and it works as discussed and expected, even with the Swift storage.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'docs/source/overview.rst'
2--- docs/source/overview.rst 2015-03-17 15:05:05 +0000
3+++ docs/source/overview.rst 2015-06-17 17:54:20 +0000
4@@ -57,3 +57,52 @@
5 This shows the data model and application interdependencies.
6
7 .. graphviz:: apps.dot
8+
9+Overview of what happens
10+------------------------
11+
12+The main entities are the Project and the Dependency.
13+
14+A dependency maps directly to a Jenkins job which produces a set of artifacts.
15+
16+Each job is associated with a job type. The job type can be thought of as a "class" while the job itself is an "instance". Each "instance" of the job
17+is linked to a dependency (one-to-one correspondence).
18+
19+Dependencies can be built on their own or as part of a project. We'll look at building a project which triggers its dependency builds first.
20+
21+Requesting a project build will::
22+
23+ * Create a ProjectBuild with data about this particular request.
24+ * For each dependency associated with this project:
25+ * If the dependency was marked to be built:
26+ * Launch a Jenkins Build of the dependency.
27+ * Create a ProjectBuildDependency linking:
28+ * The Project
29+ * The Dependency
30+ * The particular Build of this dependency (from Jenkins)
31+ * If the dependency was NOT marked to be built (use old one):
32+ * Create a ProjectBuildDependency linking:
33+ * This ProjectBuild (it implicits a link to the Project)
34+ * This Dependency
35+ * The *PREVIOUS* existing Build of this dependency (from Jenkins)
36+ * Look at the old Build
37+ * For each Artifact linked to the old Build (X):
38+ * create an ArchiveArtifact (Y) linking:
39+ * This Dependency (can get from the PBD?)
40+ * This ProjectBuildDependency
41+ * This Build (can get it from the Artifact?)
42+ * This Artifact
43+ * The current Archive
44+ * schedule a link_artifact_in_archive task with the ArchiveArtifact from the OLD build (X) as source and the ArchiveArtifact from the NEW build (Y) as destination.
45+ * Wait for notification from Jenkins about completed builds. When one is received:
46+ * For each artifact in the build as notified by Jenkins, create an Artifact item, link it to the Build. Then get the current Archive and ask it to archive the Artifact. This creates an ArchiveArtifact record linked to:
47+ * The ProjectBuildDependency
48+ * the Artifact (from the Jenkins side of things).
49+ * the Dependency (is this redundant? we have this from PBD)
50+ * The Archive itself
51+ * Note: There's a link from Build to Artifact and from Build to ArchiveArtifact, but Artifact could be used as intermediate table and remove the ArchiveArtifact -> Build link.
52+
53+
54+Requesting just a dependency build will::
55+
56+ * We don't know yet - this is to be researched.
57
58=== modified file 'docs/source/setup.rst'
59--- docs/source/setup.rst 2015-06-02 18:24:53 +0000
60+++ docs/source/setup.rst 2015-06-17 17:54:20 +0000
61@@ -11,6 +11,20 @@
62 * RabbitMQ, for message handling
63 * Celery, tasks management
64
65+Prerequisites
66+-------------
67+
68+Add this repository (for jenkins)::
69+
70+ sudo apt-get install wget
71+ wget -q -O - https://jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add -
72+ sudo sh -c 'echo deb http://pkg.jenkins-ci.org/debian binary/ > /etc/apt/sources.list.d/jenkins.list'
73+ sudo apt-get update
74+
75+Packages to install::
76+
77+ sudo apt-get install rabbitmq-server postgresql-9.3 libpq-dev tox python-dev libffi-dev python-cffi jenkins
78+
79 Environment
80 -----------
81
82
83=== modified file 'projects/forms.py'
84--- projects/forms.py 2015-05-06 16:49:56 +0000
85+++ projects/forms.py 2015-06-17 17:54:20 +0000
86@@ -81,5 +81,5 @@
87 interval = forms.ModelChoiceField(label='When to build',
88 widget=forms.RadioSelect(),
89 queryset=IntervalSchedule.objects.all(),
90- empty_label="Now",
91+ empty_label="now",
92 required=False)
93
94=== modified file 'projects/helpers.py'
95--- projects/helpers.py 2015-04-15 21:55:45 +0000
96+++ projects/helpers.py 2015-06-17 17:54:20 +0000
97@@ -1,9 +1,11 @@
98 import json
99
100+from archives.models import ArchiveArtifact
101+from archives.tasks import link_artifact_in_archive
102+from djcelery.models import PeriodicTask
103 from jenkins.tasks import build_job
104 from jenkins.models import Build
105 from projects.models import Project, ProjectDependency
106-from djcelery.models import PeriodicTask
107
108 def build_dependency(dependency, build_id=None, user=None):
109 """
110@@ -46,8 +48,8 @@
111 if automated:
112 options["phase"] = Build.FINALIZED
113
114- previous_build = project.get_current_projectbuild()
115- build = ProjectBuild.objects.create(**options)
116+ previous_project_build = project.get_current_projectbuild()
117+ project_build = ProjectBuild.objects.create(**options)
118
119 if dependencies:
120 filter_args = {"dependency__in": dependencies}
121@@ -62,12 +64,14 @@
122 if not automated:
123 for dependency in dependencies_to_build.order_by(
124 "dependency__job__pk"):
125- kwargs = {"projectbuild": build,
126+ kwargs = {"projectbuild": project_build,
127 "dependency": dependency.dependency}
128 ProjectBuildDependency.objects.create(**kwargs)
129 if queue_build:
130 build_dependency(
131- dependency.dependency, build_id=build.build_key, user=user)
132+ dependency.dependency,
133+ build_id=project_build.build_key,
134+ user=user)
135
136 # If it's automated, then we create a ProjectBuildDependency for each
137 # dependency of the project and prepopulate it with the last known build.
138@@ -77,14 +81,68 @@
139 else:
140 remaining_builds = dependencies_not_to_build
141
142+ # Walk each non-built dependency
143 for dependency in remaining_builds:
144+ # last_known_build is a jenkins.Build instance
145 last_known_build = get_last_build_for_dependency(
146- dependency, previous_build)
147- kwargs = {"projectbuild": build,
148+ dependency, previous_project_build)
149+ kwargs = {"projectbuild": project_build,
150 "dependency": dependency.dependency,
151 "build": last_known_build}
152- ProjectBuildDependency.objects.create(**kwargs)
153- return build
154+ # Link the new project build with this dependency but using the
155+ # last known build. This is done using the intermediate
156+ # ProjectBuildDependency table.
157+ new_project_build_dependency = ProjectBuildDependency.objects.create(
158+ **kwargs)
159+ # For each archived artifact (i.e. artifact already on Capomastro's
160+ # archiver, previously pulled from Jenkins), we need to create a
161+ # "copy" of the ArchiveArtifact. The two differences are: it should
162+ # be linked to the *new* project build dependency (so it's associated
163+ # with the new project build rather than the old one), and its
164+ # archived_path should match the one for the new project build.
165+ #
166+ # For this we look at the last known build's full set of artifacts,
167+ # filtering by those whose last known build matches the one we
168+ # identified earlier.
169+ if not last_known_build:
170+ continue
171+ for archiveartifact in last_known_build.archiveartifact_set.filter(
172+ projectbuild_dependency__build=last_known_build):
173+ # The filename is one of the items that changes. Use the
174+ # get_path_for_artifact method to get a filename matching the
175+ # new ProjectBuildDependency's archival path.
176+ policy = archiveartifact.archive.get_policy()
177+ new_filename = policy.get_path_for_artifact(
178+ archiveartifact.artifact,
179+ build=archiveartifact.build, dependency=dependency.dependency,
180+ projectbuild=new_project_build_dependency.projectbuild)
181+ # Build the arguments for the new ArchiveArtifact. It's clear here
182+ # that the two items that change are:
183+ # * archived_path as calculated above.
184+ # * projectbuild_dependency so it eventually links to the
185+ # new project build.
186+ # FIXME: I'm lazy and used dependency.dependency, it would be
187+ # better to use archiveartifact.dependency so we keep the "copy"
188+ # scoped to the archiveartifact.
189+ kwargs = {'archive': archiveartifact.archive,
190+ 'artifact': archiveartifact.artifact,
191+ 'archived_path': new_filename,
192+ 'archived_size': archiveartifact.archived_size,
193+ 'build': archiveartifact.build,
194+ 'projectbuild_dependency': new_project_build_dependency,
195+ 'dependency' : dependency.dependency}
196+ # Maybe we already have an ArchiveArtifact with these parameters?
197+ # if so, retrieve it.
198+ # FIXME: I don't know why this could happen (multiple artifacts),
199+ # need to research better.
200+ newaa, created = ArchiveArtifact.objects.get_or_create(**kwargs)
201+ # Finally dispatch a celery task to create the actual archive
202+ # link from the old artifact to the new. This "syncs" the
203+ # filesystem or archiver with the state that's stored in the
204+ # database.
205+ link_artifact_in_archive.delay(archiveartifact.pk, newaa.pk)
206+
207+ return project_build
208
209
210 def get_last_build_for_dependency(dependency, previous_build=None):
211@@ -92,6 +150,10 @@
212 Return the last known build for the provided ProjectDependency dependency,
213 which is defined as the current build associated with itself if it's not
214 auto-tracked, or the most recent build for auto-tracked cases.
215+
216+ arguments:
217+ dependency should be a ProjectDependency.
218+ previous_build should be a ProjectBuild, or None.
219 """
220 if dependency.auto_track:
221 if previous_build:
222
223=== modified file 'projects/templates/projects/projectbuild_form.html'
224--- projects/templates/projects/projectbuild_form.html 2015-04-16 16:36:49 +0000
225+++ projects/templates/projects/projectbuild_form.html 2015-06-17 17:54:20 +0000
226@@ -9,11 +9,11 @@
227 <div class="row">
228 <div>
229 <h2>Build or Schedule Project {{ project.name }}</h2>
230- <h3>{{ project.name }}</h3>
231 <p>{{ project.description|default:"" }}</p>
232 {% if dependency_count > 0 %}
233- <h3>Selection</h3>
234- <p>Please select all the dependencies you want to build.</p>
235+ <h3>Select dependencies to build</h3>
236+ <p>Dependencies not selected will be skipped and will default to the current versions for this project.</p>
237+ <p>Artifacts of skipped dependencies will be reprocessed for archival.</p>
238 <form action="" method="post" class="form" id="buildproject-form">
239 {% csrf_token %}
240 {% bootstrap_form form %}
241@@ -21,7 +21,6 @@
242 <button type="submit" class="btn btn-primary">Build</button>
243 {% endbuttons %}
244 </form>
245- <p>Any dependencies not selected will not be built and will default to the current built versions for this project.</p>
246 {% else %}
247 <h3>Selection</h3>
248 <p>Unable to select dependencies.</p>
249
250=== modified file 'projects/tests/test_helpers.py'
251--- projects/tests/test_helpers.py 2015-04-15 21:55:45 +0000
252+++ projects/tests/test_helpers.py 2015-06-17 17:54:20 +0000
253@@ -5,12 +5,14 @@
254 import mock
255
256 from djcelery.models import PERIOD_CHOICES, IntervalSchedule, PeriodicTask
257+from jenkins.models import Build
258 from projects.models import (
259 ProjectBuild, ProjectDependency, ProjectBuildDependency)
260 from projects.helpers import (
261 build_project, build_dependency, project_for_periodic_task)
262-from .factories import ProjectFactory, DependencyFactory
263-from jenkins.tests.factories import BuildFactory
264+from .factories import ProjectFactory, ProjectBuildFactory, DependencyFactory
265+from archives.tests.factories import ArchiveArtifactFactory
266+from jenkins.tests.factories import BuildFactory, ArtifactFactory
267
268
269 class PeriodicTaskTest(TestCase):
270@@ -220,6 +222,83 @@
271 projectbuild=build2, dependency=dependency2)
272 self.assertEqual(built_dependency2.build, built_dependency1.build)
273
274+ def test_build_project_non_automated_partial_build(self):
275+ """
276+ When some of the dependencies are selected to be built, the final
277+ ProjectBuild should have PBDs and ArchiveArtifacts for all
278+ dependencies. The non-rebuilt ones should link to previous artifacts.
279+ """
280+ project = ProjectFactory.create()
281+ dependency1 = DependencyFactory.create()
282+ pd1 = ProjectDependency.objects.create(
283+ project=project, dependency=dependency1)
284+
285+ dependency2 = DependencyFactory.create()
286+ ProjectDependency.objects.create(
287+ project=project, dependency=dependency2)
288+
289+ # When a project is built a ProjectBuildDependency appears.
290+ # We only care about dependency1 as dependency2 will be built in
291+ # the second build.
292+ first_build = ProjectBuildFactory.create(project=project)
293+ # An artifact appears and is archived
294+ artifact = ArtifactFactory.create()
295+ archive_artifact = ArchiveArtifactFactory.create(artifact=artifact)
296+ archive_artifact.build = artifact.build # Bad normalization!
297+ archive_artifact.dependency = dependency1 # Bad normalization!
298+ archive_artifact.save()
299+ archive_artifact.build.projectdependency_set.add(pd1)
300+ # Link the artifact to the projectbuilddependency
301+ first_pbd = ProjectBuildDependency.objects.create(
302+ dependency=dependency1,
303+ build=archive_artifact.build,
304+ projectbuild=first_build)
305+ archive_artifact.projectbuild_dependency = first_pbd
306+ archive_artifact.save()
307+ # Mark the project build as successfully finalized
308+ first_pbd.projectbuild.phase = Build.FINALIZED
309+ first_pbd.projectbuild.status = "SUCCESS"
310+ first_pbd.projectbuild.save()
311+
312+ # Now try the new build
313+ with mock.patch("projects.helpers.build_job") as mock_build_job, mock.patch("projects.helpers.link_artifact_in_archive") as mock_link_artifact:
314+ new_build = build_project(project, dependencies=[dependency2],
315+ queue_build=True)
316+ self.assertIsInstance(new_build, ProjectBuild)
317+
318+ # Verification time
319+ build_dependencies = ProjectBuildDependency.objects.filter(
320+ projectbuild=new_build)
321+ # New build should still have 2 build_dependencies.
322+ self.assertEqual(2, build_dependencies.count())
323+ # We only called build_job for dependency2, as dependency1 was already
324+ # built and was just re-linked
325+ mock_build_job.delay.assert_has_calls(
326+ [mock.call(dependency2.job.pk, build_id=new_build.build_key)])
327+ # Ensure both dependencies are in the new build, even though only one
328+ # of them was explicitly rebuilt.
329+ self.assertEqual(
330+ sorted([dependency1.pk, dependency2.pk]),
331+ sorted(list(build_dependencies.values_list("dependency",
332+ flat=True))))
333+ # Ensure the artifacts and all for the non-explicitly-built dep are
334+ # what we want.
335+ dep1_build = build_dependencies.filter(dependency=dependency1)[0]
336+ self.assertEqual(dep1_build.projectbuild, new_build)
337+ # The build should be the same as the existing archiveartifact's build
338+ self.assertEqual(dep1_build.build, archive_artifact.build)
339+ # The archiveartifact_set should contain one item...
340+ self.assertEqual(1, len(dep1_build.archiveartifact_set.all()))
341+ # And it should NOT be the old archiveartifact.
342+ self.assertNotIn(archive_artifact,
343+ dep1_build.archiveartifact_set.all())
344+ # Now let's get the new archiveartifact and ensure we called
345+ # link_artifacts_in_archive
346+ new_archive_artifact = dep1_build.archiveartifact_set.all()[0]
347+
348+ mock_link_artifact.delay.assert_has_calls(
349+ [mock.call(archive_artifact.pk, new_archive_artifact.pk)])
350+
351
352 class BuildDependencyTest(TestCase):
353

Subscribers

People subscribed via source and target branches