Skip to content

Commit 74c0456

Browse files
PanaetiusRalf Grubenmann
andauthored
fix(service): use temporary directory to clone project templates on project creation (#3243)
Co-authored-by: Ralf Grubenmann <[email protected]>
1 parent 5542fdd commit 74c0456

File tree

7 files changed

+34
-38
lines changed

7 files changed

+34
-38
lines changed

renku/core/template/template.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ def get_latest_reference_and_version(
403403
else:
404404
return (self.reference, self.version) if current_version < Version(self.version) else (reference, version)
405405

406-
def get_template(self, id, reference: Optional[str]) -> Optional["Template"]:
406+
def get_template(self, id, reference: Optional[str]) -> "Template":
407407
"""Return all available versions for a template id."""
408408
try:
409409
return next(t for t in self.templates if t.id == id)
@@ -491,7 +491,7 @@ def _has_template_at(self, id: str, reference: str) -> bool:
491491
else:
492492
return any(t.id == id for t in manifest.templates)
493493

494-
def get_template(self, id, reference: Optional[str]) -> Optional["Template"]:
494+
def get_template(self, id, reference: Optional[str]) -> "Template":
495495
"""Return a template at a specific reference."""
496496
if reference is not None and reference != self.reference:
497497
try:

renku/domain_model/template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def get_latest_reference_and_version(
8888
raise NotImplementedError
8989

9090
@abstractmethod
91-
def get_template(self, id, reference: Optional[str]) -> Optional["Template"]:
91+
def get_template(self, id, reference: Optional[str]) -> "Template":
9292
"""Return a template at a specific reference."""
9393
raise NotImplementedError
9494

renku/ui/service/controllers/templates_create_project.py

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@
1717
# limitations under the License.
1818
"""Renku service template create project controller."""
1919
import shutil
20-
from pathlib import Path
21-
from typing import Dict, Optional
20+
from typing import Any, Dict, Optional, cast
2221

2322
from marshmallow import EXCLUDE
2423

2524
from renku.command.init import create_from_template_local_command
25+
from renku.core import errors
26+
from renku.core.template.template import fetch_templates_source
2627
from renku.core.util.contexts import renku_project_context
27-
from renku.domain_model.template import TEMPLATE_MANIFEST, TemplatesManifest
28+
from renku.domain_model.template import Template
2829
from renku.infrastructure.repository import Repository
2930
from renku.ui.service.config import MESSAGE_PREFIX
3031
from renku.ui.service.controllers.api.abstract import ServiceCtrl
3132
from renku.ui.service.controllers.api.mixins import RenkuOperationMixin
32-
from renku.ui.service.controllers.utils.project_clone import user_project_clone
3333
from renku.ui.service.errors import UserProjectCreationError
3434
from renku.ui.service.serializers.templates import ProjectTemplateRequest, ProjectTemplateResponseRPC
3535
from renku.ui.service.utils import new_repo_push
@@ -45,11 +45,14 @@ class TemplatesCreateProjectCtrl(ServiceCtrl, RenkuOperationMixin):
4545

4646
def __init__(self, cache, user_data, request_data):
4747
"""Construct a templates read manifest controller."""
48-
self.ctx = TemplatesCreateProjectCtrl.REQUEST_SERIALIZER.load({**user_data, **request_data}, unknown=EXCLUDE)
48+
self.ctx = cast(
49+
Dict[str, Any],
50+
TemplatesCreateProjectCtrl.REQUEST_SERIALIZER.load({**user_data, **request_data}, unknown=EXCLUDE),
51+
)
4952
self.ctx["commit_message"] = f"{MESSAGE_PREFIX} init {self.ctx['project_name']}"
5053
super(TemplatesCreateProjectCtrl, self).__init__(cache, user_data, request_data)
5154

52-
self.template: Optional[Dict] = None
55+
self.template: Optional[Template] = None
5356

5457
@property
5558
def context(self):
@@ -59,16 +62,12 @@ def context(self):
5962
@property
6063
def default_metadata(self):
6164
"""Default metadata for project creation."""
62-
automated_update = True
63-
if self.template and "allow_template_update" in self.template:
64-
automated_update = self.template["allow_template_update"]
6565

6666
metadata = {
6767
"__template_source__": self.ctx["git_url"],
6868
"__template_ref__": self.ctx["ref"],
6969
"__template_id__": self.ctx["identifier"],
7070
"__namespace__": self.ctx["project_namespace"],
71-
"__automated_update__": automated_update,
7271
"__repository__": self.ctx["project_repository"],
7372
"__sanitized_project_name__": self.ctx["project_name_stripped"],
7473
"__project_slug__": self.ctx["project_slug"],
@@ -114,50 +113,49 @@ def setup_new_project(self):
114113

115114
def setup_template(self):
116115
"""Reads template manifest."""
117-
project = user_project_clone(self.user_data, self.ctx)
118-
templates = TemplatesManifest.from_path(Path(project.abs_path) / TEMPLATE_MANIFEST).get_raw_content()
116+
templates_source = fetch_templates_source(source=self.ctx["git_url"], reference=self.ctx["ref"])
119117
identifier = self.ctx["identifier"]
120-
self.template = next((template for template in templates if template["folder"] == identifier), None)
121-
if self.template is None:
118+
try:
119+
self.template = templates_source.get_template(id=identifier, reference=None)
120+
except (errors.InvalidTemplateError, errors.TemplateNotFoundError) as e:
122121
raise UserProjectCreationError(
123122
error_message=f"the template '{identifier}' does not exist in the target template's repository"
124-
)
123+
) from e
125124

126-
repository = Repository(project.abs_path)
125+
repository = Repository(templates_source.path)
127126
self.template_version = repository.head.commit.hexsha
128127

129128
# Verify missing parameters
130-
template_parameters = self.template.get("variables", {})
129+
template_parameters = set(p.name for p in self.template.parameters)
131130
provided_parameters = {p["key"]: p["value"] for p in self.ctx["parameters"]}
132-
missing_keys = list(template_parameters.keys() - provided_parameters.keys())
131+
missing_keys = list(template_parameters - provided_parameters.keys())
133132
if len(missing_keys) > 0:
134133
raise UserProjectCreationError(error_message=f"the template requires a value for '${missing_keys[0]}'")
135134

136-
return project, provided_parameters
135+
return provided_parameters
137136

138137
def new_project_push(self, project_path):
139138
"""Push new project to the remote."""
140139
return new_repo_push(project_path, self.ctx["new_project_url_with_auth"])
141140

142141
def new_project(self):
143142
"""Create new project from template."""
144-
template_project, provided_parameters = self.setup_template()
143+
provided_parameters = self.setup_template()
144+
assert self.template is not None
145145
new_project = self.setup_new_project()
146146
new_project_path = new_project.abs_path
147147

148-
source_path = template_project.abs_path / self.ctx["identifier"]
149-
150148
with renku_project_context(new_project_path):
151149
create_from_template_local_command().build().execute(
152-
source_path,
150+
self.template.path,
153151
name=self.ctx["project_name"],
154152
namespace=self.ctx["project_namespace"],
155153
metadata=provided_parameters,
156154
default_metadata=self.default_metadata,
157155
custom_metadata=self.ctx["project_custom_metadata"],
158156
template_version=self.template_version,
159-
immutable_template_files=self.template.get("immutable_template_files", []), # type: ignore[union-attr]
160-
automated_template_update=self.template.get("allow_template_update", True), # type: ignore[union-attr]
157+
immutable_template_files=self.template.immutable_files,
158+
automated_template_update=self.template.allow_update,
161159
user=self.git_user,
162160
initial_branch=self.ctx["initial_branch"],
163161
commit_message=self.ctx["commit_message"],

tests/fixtures/templates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def get_latest_reference_and_version(
191191
version = str(max(self._versions))
192192
return version, version
193193

194-
def get_template(self, id, reference: Optional[str]) -> Optional[Template]:
194+
def get_template(self, id, reference: Optional[str]) -> Template:
195195
"""Return a template at a specific reference."""
196196
if not reference:
197197
reference = self.reference

tests/service/controllers/test_templates_create_project.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ def test_template_create_project_ctrl(ctrl_init, svc_client_templates_creation,
6666
"ref",
6767
"new_project_url_with_auth",
6868
"url_with_auth",
69-
"user_id",
7069
}
7170
assert expected_context.issubset(set(ctrl.context.keys()))
7271

@@ -76,7 +75,6 @@ def test_template_create_project_ctrl(ctrl_init, svc_client_templates_creation,
7675
"__template_ref__",
7776
"__template_id__",
7877
"__namespace__",
79-
"__automated_update__",
8078
"__repository__",
8179
"__sanitized_project_name__",
8280
"__project_slug__",

tests/service/views/test_templates_views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def test_create_project_from_template_failures(svc_client_templates_creation):
194194
response = svc_client.post("/templates.create_project", data=json.dumps(payload_missing_project), headers=headers)
195195
assert 200 == response.status_code
196196
assert {"error"} == set(response.json.keys())
197-
assert UserProjectCreationError.code == response.json["error"]["code"]
197+
assert UserProjectCreationError.code == response.json["error"]["code"], response.json
198198
assert "project name" in response.json["error"]["devMessage"].lower()
199199

200200
# NOTE: fail on wrong git url - unexpected when invoked from the UI
@@ -204,7 +204,7 @@ def test_create_project_from_template_failures(svc_client_templates_creation):
204204
response = svc_client.post("/templates.create_project", data=json.dumps(payload_wrong_repo), headers=headers)
205205
assert 200 == response.status_code
206206
assert {"error"} == set(response.json.keys())
207-
assert UserProjectCreationError.code == response.json["error"]["code"]
207+
assert UserProjectCreationError.code == response.json["error"]["code"], response.json
208208
assert "git_url" in response.json["error"]["devMessage"]
209209

210210
# NOTE: missing fields -- unlikely to happen. If that is the case, we should determine if it's a user error or not
@@ -225,7 +225,7 @@ def test_create_project_from_template_failures(svc_client_templates_creation):
225225
response = svc_client.post("/templates.create_project", data=json.dumps(payload_fake_id), headers=headers)
226226
assert 200 == response.status_code
227227
assert {"error"} == set(response.json.keys())
228-
assert UserProjectCreationError.code == response.json["error"]["code"]
228+
assert UserProjectCreationError.code == response.json["error"]["code"], response.json
229229
assert "does not exist" in response.json["error"]["devMessage"]
230230
assert fake_identifier in response.json["error"]["devMessage"]
231231

@@ -239,6 +239,6 @@ def test_create_project_from_template_failures(svc_client_templates_creation):
239239

240240
assert 200 == response.status_code
241241
assert {"error"} == set(response.json.keys())
242-
assert UserProjectCreationError.code == response.json["error"]["code"]
242+
assert UserProjectCreationError.code == response.json["error"]["code"], response.json
243243
assert "does not exist" in response.json["error"]["devMessage"]
244244
assert fake_identifier in response.json["error"]["devMessage"]

tests/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,18 +116,18 @@ def modified_environ(*remove, **update):
116116
"""
117117
env = os.environ
118118
update = update or {}
119-
remove_list = list(remove) or []
119+
remove_set = set(remove or [])
120120

121121
# List of environment variables being updated or removed.
122-
stomped = (set(update.keys()) | set(remove_list)) & set(env.keys())
122+
stomped = (set(update.keys()) | remove_set) & set(env.keys())
123123
# Environment variables and values to restore on exit.
124124
update_after = {k: env[k] for k in stomped}
125125
# Environment variables and values to remove on exit.
126126
remove_after = frozenset(k for k in update if k not in env)
127127

128128
try:
129129
env.update(update)
130-
[env.pop(k, None) for k in remove_list]
130+
[env.pop(k, None) for k in remove_set]
131131
yield
132132
finally:
133133
env.update(update_after)

0 commit comments

Comments
 (0)