Merge ~lgp171188/lpci:contain-config-file-path-under-project-dir into lpci:main

Proposed by Guruprasad
Status: Merged
Merged at revision: dcf23ecf8ab5a7a6053973ac6ac1a8488aa75833
Proposed branch: ~lgp171188/lpci:contain-config-file-path-under-project-dir
Merge into: lpci:main
Diff against target: 224 lines (+129/-1)
5 files modified
NEWS.rst (+2/-0)
lpcraft/commands/tests/test_clean.py (+29/-0)
lpcraft/commands/tests/test_run.py (+61/-1)
lpcraft/config.py (+17/-0)
lpcraft/tests/test_config.py (+20/-0)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+418035@code.launchpad.net

Commit message

Require the configuration file to be under the project directory

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

There is one broken test and a couple of other things to have a look at.

review: Needs Fixing
Revision history for this message
Jürgen Gmach (jugmac00) :
Revision history for this message
Guruprasad (lgp171188) :
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Thanks Guruprasad! Great discussion. I think everything should be clear now.

Revision history for this message
Guruprasad (lgp171188) wrote :

Replied to some review comments.

Revision history for this message
Guruprasad (lgp171188) wrote (last edit ):

Jürgen, I have pushed 3 new commits to address all the open review comments. The resulting diff for this merge proposal is much simpler and cleaner.

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

I think you need to rebase your MP.

Revision history for this message
Guruprasad (lgp171188) wrote :

Jürgen, I have rebased the MP and fixed the merge conflict now. Afaics, all the open review comments have been addressed. Please review again.

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

Looks good!

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

Please update RTD and create a new Snap once you have merged the MP.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/NEWS.rst b/NEWS.rst
2index 35216fe..daf4007 100644
3--- a/NEWS.rst
4+++ b/NEWS.rst
5@@ -14,6 +14,8 @@ Version history
6
7 - tox: The ``pip-compile`` env now upgrades the project's dependencies.
8
9+- Require the configuration file to be present under the project directory.
10+
11 0.0.5 (2022-03-30)
12 ====================
13
14diff --git a/lpcraft/commands/tests/test_clean.py b/lpcraft/commands/tests/test_clean.py
15index 5a11555..c445095 100644
16--- a/lpcraft/commands/tests/test_clean.py
17+++ b/lpcraft/commands/tests/test_clean.py
18@@ -24,6 +24,35 @@ class TestClean(CommandBaseTestCase):
19 os.chdir(self.tmp_project_path)
20 self.addCleanup(os.chdir, cwd)
21
22+ def test_config_file_not_under_project_directory(self):
23+ paths = [
24+ "/",
25+ "/etc/init.d",
26+ "../../foo",
27+ "a/b/c/../../../../d",
28+ ]
29+
30+ for path in paths:
31+ config_file = f"{path}/config.yaml"
32+ result = self.run_command(
33+ "clean",
34+ "-c",
35+ config_file,
36+ )
37+ config_file_path = Path(config_file).resolve()
38+ self.assertThat(
39+ result,
40+ MatchesStructure.byEquality(
41+ exit_code=1,
42+ errors=[
43+ ConfigurationError(
44+ f"'{config_file_path}' is not in the subpath of "
45+ f"'{self.tmp_project_path}'."
46+ )
47+ ],
48+ ),
49+ )
50+
51 def test_missing_config_file(self):
52 result = self.run_command("clean")
53
54diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py
55index cda2aaa..27baf8b 100644
56--- a/lpcraft/commands/tests/test_run.py
57+++ b/lpcraft/commands/tests/test_run.py
58@@ -40,7 +40,7 @@ class LocalExecuteRun:
59 *,
60 cwd: Optional[Path] = None,
61 env: Optional[Dict[str, Optional[str]]] = None,
62- **kwargs: Any
63+ **kwargs: Any,
64 ) -> "subprocess.CompletedProcess[AnyStr]":
65 run_kwargs = kwargs.copy()
66 run_kwargs["cwd"] = self.override_cwd
67@@ -83,6 +83,35 @@ class RunBaseTestCase(CommandBaseTestCase):
68
69
70 class TestRun(RunBaseTestCase):
71+ def test_config_file_not_under_project_directory(self):
72+ paths = [
73+ "/",
74+ "/etc/init.d",
75+ "../../foo",
76+ "a/b/c/../../../../d",
77+ ]
78+
79+ for path in paths:
80+ config_file = f"{path}/config.yaml"
81+ result = self.run_command(
82+ "run",
83+ "-c",
84+ config_file,
85+ )
86+ config_file_path = Path(config_file).resolve()
87+ self.assertThat(
88+ result,
89+ MatchesStructure.byEquality(
90+ exit_code=1,
91+ errors=[
92+ ConfigurationError(
93+ f"'{config_file_path}' is not in the subpath of "
94+ f"'{self.tmp_project_path}'."
95+ )
96+ ],
97+ ),
98+ )
99+
100 def test_missing_config_file(self):
101 result = self.run_command("run")
102
103@@ -1630,6 +1659,37 @@ class TestRun(RunBaseTestCase):
104
105
106 class TestRunOne(RunBaseTestCase):
107+ def test_config_file_not_under_project_directory(self):
108+ paths = [
109+ "/",
110+ "/etc/init.d",
111+ "../../foo",
112+ "a/b/c/../../../../d",
113+ ]
114+
115+ for path in paths:
116+ config_file = f"{path}/config.yaml"
117+ result = self.run_command(
118+ "run-one",
119+ "-c",
120+ config_file,
121+ "test",
122+ "0",
123+ )
124+ config_file_path = Path(config_file).resolve()
125+ self.assertThat(
126+ result,
127+ MatchesStructure.byEquality(
128+ exit_code=1,
129+ errors=[
130+ ConfigurationError(
131+ f"'{config_file_path}' is not in the subpath of "
132+ f"'{self.tmp_project_path}'."
133+ )
134+ ],
135+ ),
136+ )
137+
138 def test_missing_config_file(self):
139 result = self.run_command("run-one", "test", "0")
140
141diff --git a/lpcraft/config.py b/lpcraft/config.py
142index 0777dd3..07f17ff 100644
143--- a/lpcraft/config.py
144+++ b/lpcraft/config.py
145@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Union
146 import pydantic
147 from pydantic import StrictStr
148
149+from lpcraft.errors import ConfigurationError
150 from lpcraft.utils import load_yaml
151
152
153@@ -127,5 +128,21 @@ class Config(ModelConfigDefaults):
154 @classmethod
155 def load(cls, path: Path) -> "Config":
156 """Load config from the indicated file name."""
157+ # XXX lgp171188 2022-03-31: it is not a good idea to evaluate
158+ # `Path.cwd()` in multiple places to determine the project directory.
159+ # This could be made available as an attribute on the `Config` class
160+ # instead.
161+ project_dir = Path.cwd()
162+ resolved_path = path.resolve()
163+ try:
164+ # XXX lgp171188 2022-04-04: There is a new method,
165+ # Path.is_relative_to() in Python 3.9+, which does
166+ # exactly what we need. Once we drop support
167+ # for Python 3.8, we should switch to that instead.
168+ resolved_path.relative_to(project_dir)
169+ except ValueError:
170+ raise ConfigurationError(
171+ f"'{resolved_path}' is not in the subpath of '{project_dir}'."
172+ )
173 content = load_yaml(path)
174 return cls.parse_obj(content)
175diff --git a/lpcraft/tests/test_config.py b/lpcraft/tests/test_config.py
176index 96025b6..6867024 100644
177--- a/lpcraft/tests/test_config.py
178+++ b/lpcraft/tests/test_config.py
179@@ -1,6 +1,7 @@
180 # Copyright 2021-2022 Canonical Ltd. This software is licensed under the
181 # GNU General Public License version 3 (see the file LICENSE).
182
183+import os
184 from datetime import timedelta
185 from pathlib import Path
186 from textwrap import dedent
187@@ -16,18 +17,37 @@ from testtools.matchers import (
188 )
189
190 from lpcraft.config import Config, OutputDistributeEnum
191+from lpcraft.errors import CommandError
192
193
194 class TestConfig(TestCase):
195 def setUp(self):
196 super().setUp()
197 self.tempdir = Path(self.useFixture(TempDir()).path)
198+ # `Path.cwd()` is assumed as the project directory.
199+ # So switch to the created project directory.
200+ os.chdir(self.tempdir)
201
202 def create_config(self, text):
203 path = self.tempdir / ".launchpad.yaml"
204 path.write_text(text)
205 return path
206
207+ def test_load_config_not_under_project_dir(self):
208+ paths_outside_project_dir = [
209+ "/",
210+ "/etc/init.d",
211+ "../../foo",
212+ "a/b/c/../../../../d",
213+ ]
214+ for path in paths_outside_project_dir:
215+ config_file = Path(path) / "config.yaml"
216+ self.assertRaises(
217+ CommandError,
218+ Config.load,
219+ config_file,
220+ )
221+
222 def test_load(self):
223 path = self.create_config(
224 dedent(

Subscribers

People subscribed via source and target branches