Merge ~tintou/git-build-recipe:master into git-build-recipe:master

Proposed by Corentin Noël
Status: Needs review
Proposed branch: ~tintou/git-build-recipe:master
Merge into: git-build-recipe:master
Diff against target: 230 lines (+92/-25)
2 files modified
gitbuildrecipe/recipe.py (+55/-19)
gitbuildrecipe/tests/test_recipe.py (+37/-6)
Reviewer Review Type Date Requested Status
Colin Watson Needs Fixing
Review via email: mp+351699@code.launchpad.net

Commit message

Handle submodules

It is possible to fetch the submodules before building the package by specifying submodule-strategy {normal, recursive} in the header of the recipe

The default behavior is not changed

Description of the change

I had to refactor the handling of the `deb-version` optional variable as there are now two optional variables.
by default, there is no additional command run, if the submodule strategy is changed then the repository is synced and updated (the behavior is inspired from what is described in https://docs.gitlab.com/ee/ci/yaml/README.html#git-submodule-strategy )

I haven't tested it on a real repository though.

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

Most of these are nits, but I'd really prefer to have end-to-end functional tests before merging this (although I've tested it locally and it does seem to work fine).

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

Hm. There are going to be some more serious problems too.

URL filtering
-------------

In general, when we have user-supplied URLs, we normally filter them so that people can't e.g. pass in file:/// URLs and use those to construct some kind of exploit against the local system. For URLs in the recipe itself, we handle those by parsing the recipe in Launchpad and only allowing references to repositories hosted on Launchpad. But for submodules, that typically doesn't work; we'll need to expect that repositories have references to external URLs, and it's hard to filter those in advance (especially for the recursive strategy).

Now, it's possible we can get away without caring about this, since the builders on which we run git-build-recipe are throwaway VMs that don't have any secrets. But it needs to be documented for the benefit of other users of git-build-recipe, and it would be good if you could do some research to find if there's any reasonable way to filter submodule URLs before cloning them.

Recipe parsing in Launchpad
---------------------------

Launchpad currently takes the shortcut of parsing the supplied recipe using bzr-builder, and then mangling it slightly to account for the minor differences in git-build-recipe's format. We'll need to check whether that process needs any changes to account for this extension to the format. I think we ought to check that before landing this, because perhaps there will turn out to be some way to extend the format that's more compatible than others.

External access from Launchpad builders
---------------------------------------

The bad news is that this is the hard bit. The good news is that it doesn't need to block landing this code; it may well make the feature less useful until something's done about it, but the fixes for this won't be in git-build-recipe.

In general, Launchpad builders don't have access to the external internet. So this will for submodules that are hosted on Launchpad (using lp: or https://git.launchpad.net/ in submodule URL configuration), but it won't work for external URLs.

We'll need to have a policy discussion about whether we want to allow these on Launchpad. I'm leaning towards yes, because git submodules are pinned by their commit ID which is pretty good protection against the usual problems with external access for things like build reproducibility, but we still need to have the discussion in the Launchpad team and reach an agreement.

After that, there would be technical work to be done to implement this. We could reuse the proxy that we use to allow snap builders to have external internet access, perhaps dispatching it only to recipes that have a submodule-strategy. That would require changes in lib/lp/code/model/recipebuilder.py (similar to lib/lp/snappy/model/snapbuildbehaviour.py) in lp:launchpad, and to the corresponding bits of lp:launchpad-buildd.

Revision history for this message
Sergey Ponomarev (stokito) wrote :

Hello, is any progress on this? We need it and will be happy if you finally fix that.
Thank in advance

Revision history for this message
Dimitri John Ledkov (xnox) wrote :

About URL filtering

gitconfig allows to globally rewrite urls/protocols, which is under the control of the system/user, and not of the repository being cloned.

such that one could rewrite them to invalid ones, or force all of them to go through a custom protocol which then implements the filtering.

But that would require careful engineering & security penetration testing.

Unmerged commits

b1f7355... by Corentin Noël on 2018-07-28

Handle submodules

It is possible to fetch the submodules before building the package by specifying submodule-strategy {normal, recursive} in the header of the recipe

The default behavior is not changed

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/gitbuildrecipe/recipe.py b/gitbuildrecipe/recipe.py
2index d1700f2..3f012ff 100644
3--- a/gitbuildrecipe/recipe.py
4+++ b/gitbuildrecipe/recipe.py
5@@ -281,6 +281,11 @@ class CheckoutFailed(GitCommandFailed):
6 _cmd = "checkout"
7
8
9+class SubmoduleFailed(GitCommandFailed):
10+
11+ _cmd = "submodule"
12+
13+
14 class MergeFailed(GitCommandFailed):
15
16 _cmd = "merge"
17@@ -320,6 +325,20 @@ def pull_or_clone(base_branch, target_path):
18 base_branch.git_call("checkout", base_branch.commit)
19 except subprocess.CalledProcessError as e:
20 raise CheckoutFailed(e.output)
21+ if base_branch.submodule_strategy is not None:
22+ if base_branch.submodule_strategy == "recursive":
23+ try:
24+ base_branch.git_call("submodule", "sync", "--recursive")
25+ base_branch.git_call("submodule", "update", "--init",
26+ "--recursive")
27+ except subprocess.CalledProcessError as e:
28+ raise SubmoduleFailed(e.output)
29+ else:
30+ try:
31+ base_branch.git_call("submodule", "sync")
32+ base_branch.git_call("submodule", "update", "--init")
33+ except subprocess.CalledProcessError as e:
34+ raise SubmoduleFailed(e.output)
35
36
37 def fetch_branches(child_branch):
38@@ -608,7 +627,7 @@ class RecipeBranch:
39 `self.url`.
40 """
41
42- def __init__(self, name, url, revspec=None):
43+ def __init__(self, name, url, submodule_strategy=None, revspec=None):
44 """Create a `RecipeBranch`.
45
46 :param name: The name for the branch, or None if it is the root.
47@@ -618,6 +637,7 @@ class RecipeBranch:
48 """
49 self.name = name
50 self.url = url
51+ self.submodule_strategy = submodule_strategy
52 self.revspec = revspec
53 self.child_branches = []
54 self.commit = None
55@@ -796,13 +816,14 @@ class RecipeBranch:
56 class BaseRecipeBranch(RecipeBranch):
57 """The `RecipeBranch` that is at the root of a recipe."""
58
59- def __init__(self, url, deb_version, format_version, revspec=None):
60+ def __init__(self, url, deb_version, format_version,
61+ submodule_strategy=None, revspec=None):
62 """Create a `BaseRecipeBranch`.
63
64 :param deb_version: The template to use for the version number.
65 Should be None for anything except the root branch.
66 """
67- super().__init__(None, url, revspec=revspec)
68+ super().__init__(None, url, submodule_strategy, revspec=revspec)
69 self.deb_version = deb_version
70 self.format_version = format_version
71 if not urlparse(url).scheme:
72@@ -910,8 +931,9 @@ class RecipeParser:
73 self.current_indent_level = 0
74 self.seen_nicks = set()
75 self.seen_paths = {".": 1}
76- version, deb_version = self.parse_header()
77+ version, deb_version, submodule_strategy = self.parse_header()
78 self.version = version
79+ self.submodule_strategy = submodule_strategy
80 last_instruction = None
81 active_branches = []
82 last_branch = None
83@@ -940,7 +962,8 @@ class RecipeParser:
84 revspec = self.parse_optional_revspec()
85 self.new_line()
86 last_branch = BaseRecipeBranch(
87- url, deb_version, self.version, revspec=revspec)
88+ url, deb_version, self.version, self.submodule_strategy,
89+ revspec=revspec)
90 active_branches = [last_branch]
91 last_instruction = ""
92 else:
93@@ -968,7 +991,8 @@ class RecipeParser:
94 else:
95 revspec = self.parse_optional_revspec()
96 self.new_line(instruction)
97- last_branch = RecipeBranch(branch_id, url, revspec=revspec)
98+ last_branch = RecipeBranch(branch_id, url,
99+ self.submodule_strategy, revspec=revspec)
100 if instruction == NEST_INSTRUCTION:
101 active_branches[-1].nest_branch(location, last_branch)
102 elif instruction == MERGE_INSTRUCTION:
103@@ -994,9 +1018,14 @@ class RecipeParser:
104 if version > self.NEWEST_VERSION:
105 self.throw_parse_error("Unknown format: '%s'" % str(version))
106 self.take_chars(len(version_str))
107- deb_version = self.parse_optional_deb_version()
108+ options = self.parse_optional_values()
109+ deb_version = options.get('deb-version', None)
110+ submodule_strategy = options.get('submodule-strategy', None)
111+ if submodule_strategy not in (None, 'normal', 'recursive'):
112+ self.throw_parse_error("Unknown submodule strategy: '%s'" %
113+ str(submodule_strategy))
114 self.new_line()
115- return version, deb_version
116+ return version, deb_version, submodule_strategy
117
118 def parse_instruction(self, permitted_instructions=None):
119 if self.version < 0.2:
120@@ -1079,17 +1108,24 @@ class RecipeParser:
121 self.parse_whitespace("the revspec")
122 return self.take_to_whitespace("the revspec")
123
124- def parse_optional_deb_version(self):
125- self.parse_whitespace("'deb-version'", require=False)
126- actual = self.peek_to_whitespace()
127- if actual is None:
128- # End of line, no version
129- return None
130- if actual != "deb-version":
131- self.throw_expecting_error("deb-version", actual)
132- self.take_chars(len("deb-version"))
133- self.parse_whitespace("a value for 'deb-version'")
134- return self.take_to_whitespace("a value for 'deb-version'")
135+ def parse_optional_values(self):
136+ options = dict()
137+ expected_options = ("deb-version", "submodule-strategy")
138+ for i in range(len(expected_options)):
139+ self.parse_whitespace("next option", require=False)
140+ actual = self.peek_to_whitespace()
141+ if actual is None:
142+ # End of line
143+ continue;
144+ else:
145+ if actual not in expected_options:
146+ self.throw_expecting_error("' or '".join(expected_options),
147+ actual)
148+ self.take_chars(len(actual))
149+ self.parse_whitespace("a value for '%s'" % actual)
150+ options[actual] = self.take_to_whitespace(
151+ "a value for '%s'" % actual)
152+ return options
153
154 def parse_optional_revspec(self):
155 self.parse_whitespace(None, require=False)
156diff --git a/gitbuildrecipe/tests/test_recipe.py b/gitbuildrecipe/tests/test_recipe.py
157index ed7634d..fdee41e 100644
158--- a/gitbuildrecipe/tests/test_recipe.py
159+++ b/gitbuildrecipe/tests/test_recipe.py
160@@ -95,6 +95,17 @@ class RecipeParserTests(GitTestCase):
161 branch = "http://foo.org/\n"
162 self.get_recipe(header + branch)
163
164+ def test_parses_reversed_strategy_deb_version(self):
165+ header = ("# git-build-recipe format 0.4 submodule-strategy normal "
166+ "deb-version " + self.deb_version + "\n")
167+ branch = "http://foo.org/\n"
168+ self.get_recipe(header + branch)
169+
170+ def test_parses_missing_deb_version_with_strategy(self):
171+ header = "# git-build-recipe format 0.4 submodule-strategy normal\n"
172+ branch = "http://foo.org/\n"
173+ self.get_recipe(header + branch)
174+
175 def tests_rejects_non_comment_to_start(self):
176 self.assertParseError(1, 1, "Expecting '#', got 'g'",
177 self.get_recipe, "git-build-recipe")
178@@ -128,9 +139,9 @@ class RecipeParserTests(GitTestCase):
179 self.get_recipe,
180 "# git-build-recipe format 10000 deb-version 1\n")
181
182- def test_rejects_invalid_deb_version_marker(self):
183- self.assertParseError(1, 31, "Expecting 'deb-version', "
184- "got 'deb'", self.get_recipe,
185+ def test_rejects_invalid_parameter(self):
186+ self.assertParseError(1, 31, "Expecting 'deb-version' or "
187+ "'submodule-strategy', got 'deb'", self.get_recipe,
188 "# git-build-recipe format 0.1 deb")
189
190 def tests_rejects_no_deb_version_value(self):
191@@ -138,9 +149,19 @@ class RecipeParserTests(GitTestCase):
192 "a value for 'deb-version'", self.get_recipe,
193 "# git-build-recipe format 0.1 deb-version")
194
195- def tests_rejects_extra_text_after_deb_version(self):
196- self.assertParseError(1, 45, "Expecting the end of the line, "
197- "got 'foo'", self.get_recipe,
198+ def test_unknown_submodule_strategy(self):
199+ self.assertParseError(1, 53, "Unknown submodule strategy: 'foo'",
200+ self.get_recipe,
201+ "# git-build-recipe format 0.4 submodule-strategy foo\n")
202+
203+ def tests_rejects_no_submodule_strategy_value(self):
204+ self.assertParseError(1, 49, "End of line while looking for "
205+ "a value for 'submodule-strategy'", self.get_recipe,
206+ "# git-build-recipe format 0.4 submodule-strategy")
207+
208+ def tests_rejects_extra_parameter(self):
209+ self.assertParseError(1, 45, "Expecting 'deb-version' or "
210+ "'submodule-strategy', got 'foo'", self.get_recipe,
211 "# git-build-recipe format 0.1 deb-version 1 foo")
212
213 def tests_rejects_indented_base_branch(self):
214@@ -539,6 +560,16 @@ class BuildTreeTests(GitTestCase):
215 self.assertEqual(commit, target.rev_parse("HEAD"))
216 self.assertEqual(commit, base_branch.commit)
217
218+ def test_build_tree_single_branch_recursive(self):
219+ source = GitRepository("source")
220+ commit = source.commit("one")
221+ base_branch = BaseRecipeBranch("source", "1", 0.2, "recursive")
222+ build_tree(base_branch, "target")
223+ self.assertThat("target", PathExists())
224+ target = GitRepository("target", allow_create=False)
225+ self.assertEqual(commit, target.rev_parse("HEAD"))
226+ self.assertEqual(commit, base_branch.commit)
227+
228 def test_build_tree_single_branch_dir_not_branch(self):
229 source = GitRepository("source")
230 source.commit("one")

Subscribers

People subscribed via source and target branches